plugin XEP-0166: (jingle) first draft. Not all actions are managed yet
This commit is contained in:
parent
66ad169227
commit
0c5ec666c2
1 changed files with 673 additions and 0 deletions
673
src/plugins/plugin_xep_0166.py
Normal file
673
src/plugins/plugin_xep_0166.py
Normal file
|
|
@ -0,0 +1,673 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# SAT plugin for Jingle (XEP-0166)
|
||||
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 Jérôme Poisson (goffi@goffi.org)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from sat.core.i18n import _, D_
|
||||
from sat.core.constants import Const as C
|
||||
from sat.core.log import getLogger
|
||||
from sat.tools import xml_tools
|
||||
log = getLogger(__name__)
|
||||
from sat.core import exceptions
|
||||
from twisted.words.protocols.jabber import jid
|
||||
# from twisted.words.protocols import jabber
|
||||
# from twisted.words.xish import domish
|
||||
from twisted.internet import defer
|
||||
# from wokkel import disco, iwokkel, data_form, compat
|
||||
from wokkel import disco, iwokkel, compat
|
||||
from twisted.words.protocols.jabber import error
|
||||
from twisted.words.protocols.jabber import xmlstream
|
||||
# from sat.core import exceptions
|
||||
# from sat.memory.memory import Sessions
|
||||
# from uuid import uuid4
|
||||
# from sat.tools import xml_tools
|
||||
from collections import namedtuple
|
||||
import uuid
|
||||
import time
|
||||
|
||||
from zope.interface import implements
|
||||
|
||||
|
||||
|
||||
IQ_SET = '/iq[@type="set"]'
|
||||
NS_JINGLE = "urn:xmpp:jingle:1"
|
||||
JINGLE_REQUEST = IQ_SET + '/jingle[@xmlns="' + NS_JINGLE + '"]'
|
||||
STATE_PENDING = "PENDING"
|
||||
STATE_ACTIVE = "ACTIVE"
|
||||
STATE_ENDED = "ENDED"
|
||||
INITIATOR = "initiator"
|
||||
RESPONDER = "responder"
|
||||
CONFIRM_TXT = D_("{entity} want to start a jingle session with you, do you accept ?")
|
||||
|
||||
PLUGIN_INFO = {
|
||||
"name": "Jingle",
|
||||
"import_name": "XEP-0166",
|
||||
"type": "XEP",
|
||||
"protocols": ["XEP-0166"],
|
||||
"main": "XEP_0166",
|
||||
"handler": "yes",
|
||||
"description": _("""Implementation of Jingle""")
|
||||
}
|
||||
|
||||
|
||||
TransportData = namedtuple('TransportData', ('namespace', 'handler', 'priority'))
|
||||
|
||||
|
||||
class XEP_0166(object):
|
||||
TRANSPORT_DATAGRAM='UDP'
|
||||
TRANSPORT_STREAMING='TCP'
|
||||
REASON_SUCCESS='success'
|
||||
REASON_DECLINE='decline'
|
||||
REASON_FAILED_APPLICATION='failed-application'
|
||||
REASON_FAILED_TRANSPORT='failed-transport'
|
||||
A_SESSION_INITIATE = "session-initiate"
|
||||
A_SESSION_ACCEPT = "session-accept"
|
||||
A_SESSION_TERMINATE = "session-terminate"
|
||||
# non standard actions
|
||||
A_PREPARE_INITIATOR = "prepare-initiator" # initiator must prepare tranfer
|
||||
A_PREPARE_RESPONDER = "prepare-responder" # responder must prepare tranfer
|
||||
A_ACCEPTED_ACK = "accepted-ack" # session accepted ack has been received from initiator
|
||||
A_START = "start" # application can start
|
||||
|
||||
def __init__(self, host):
|
||||
log.info(_("plugin Jingle initialization"))
|
||||
self.host = host
|
||||
self._applications = {} # key: namespace, value: application data
|
||||
self._transports = {} # key: namespace, value: transport data
|
||||
# we also keep transports by type, they are then sorted by priority
|
||||
self._type_transports = { XEP_0166.TRANSPORT_DATAGRAM: [],
|
||||
XEP_0166.TRANSPORT_STREAMING: [],
|
||||
}
|
||||
|
||||
def profileConnected(self, profile):
|
||||
client = self.host.getClient(profile)
|
||||
client.jingle_sessions = {} # key = sid, value = session_data
|
||||
|
||||
def getHandler(self, profile):
|
||||
return XEP_0166_handler(self)
|
||||
|
||||
def _delSession(self, client, sid):
|
||||
try:
|
||||
del client.jingle_sessions[sid]
|
||||
except KeyError:
|
||||
log.debug(u"Jingle session id [{}] is unknown, nothing to delete".format(sid))
|
||||
else:
|
||||
log.debug(u"Jingle session id [{}] deleted".format(sid))
|
||||
|
||||
## helpers methods to build stanzas ##
|
||||
|
||||
def _buildJingleElt(self, client, session, action):
|
||||
iq_elt = compat.IQ(client.xmlstream, 'set')
|
||||
iq_elt['from'] = client.jid.full()
|
||||
iq_elt['to'] = session['to_jid'].full()
|
||||
jingle_elt = iq_elt.addElement("jingle", NS_JINGLE)
|
||||
jingle_elt["sid"] = session['id']
|
||||
jingle_elt['action'] = action
|
||||
return iq_elt, jingle_elt
|
||||
|
||||
def sendError(self, error_condition, sid, request, profile):
|
||||
"""Send error stanza
|
||||
|
||||
@param error_condition: one of twisted.words.protocols.jabber.error.STANZA_CONDITIONS keys
|
||||
@param sid(unicode,None): jingle session id, or None, if session must not be destroyed
|
||||
@param request(domish.Element): original request
|
||||
@param profile: %(doc_profile)s
|
||||
"""
|
||||
client = self.host.getClient(profile)
|
||||
iq_elt = error.StanzaError(error_condition).toResponse(request)
|
||||
if error.STANZA_CONDITIONS[error_condition]['type'] == 'cancel' and sid:
|
||||
self._delSession(client, sid)
|
||||
log.warning(u"Error while managing jingle session, cancelling: {condition}".format(error_condition))
|
||||
client.xmlstream.send(iq_elt)
|
||||
|
||||
def terminate(self, reason, session, profile):
|
||||
"""Terminate the session
|
||||
|
||||
send the session-terminate action, and delete the session data
|
||||
@param reason(unicode, list[domish.Element]): if unicode, will be transformed to an element
|
||||
if a list of element, add them as children of the <reason/> element
|
||||
@param session(dict): data of the session
|
||||
@param profile: %(doc_profile)s
|
||||
"""
|
||||
client = self.host.getClient(profile)
|
||||
iq_elt, jingle_elt = self._buildJingleElt(client, session, XEP_0166.A_SESSION_TERMINATE)
|
||||
reason_elt = jingle_elt.addElement('reason')
|
||||
if isinstance(reason, basestring):
|
||||
reason_elt.addElement(reason)
|
||||
else:
|
||||
for elt in reason:
|
||||
reason_elt.addChild(elt)
|
||||
self._delSession(client, session['id'])
|
||||
d = iq_elt.send()
|
||||
return d
|
||||
|
||||
## errors which doesn't imply a stanza sending ##
|
||||
|
||||
def _iqError(self, failure, sid, client):
|
||||
"""Called when we got an <iq/> error
|
||||
|
||||
@param failure(failure.Failure): the exceptions raised
|
||||
@param sid(unicode): jingle session id
|
||||
@param profile: %(doc_client)s
|
||||
"""
|
||||
log.warning(u"Error while sending jingle <iq/> stanza: {failure}".format(failure=failure.value))
|
||||
self._delSession(client, sid)
|
||||
|
||||
def _jingleErrorCb(self, fail, sid, request, client):
|
||||
"""Called when something is going wrong while parsing jingle request
|
||||
|
||||
The error condition depend of the exceptions raised:
|
||||
exceptions.DataError raise a bad-request condition
|
||||
@param fail(failure.Failure): the exceptions raised
|
||||
@param sid(unicode): jingle session id
|
||||
@param request(domsih.Element): jingle request
|
||||
@param client: %(doc_client)s
|
||||
"""
|
||||
log.warning("Error while processing jingle request")
|
||||
if isinstance(fail, exceptions.DataError):
|
||||
self.sendError('bad-request', sid, request, client.profile)
|
||||
else:
|
||||
log.error("Unmanaged jingle exception")
|
||||
self._delSession(client, sid)
|
||||
raise fail
|
||||
|
||||
## methods used by other plugins ##
|
||||
|
||||
def registerApplication(self, namespace, handler):
|
||||
"""Register an application plugin
|
||||
|
||||
@param namespace(unicode): application namespace managed by the plugin
|
||||
@param handler(object): instance of a class which manage the application.
|
||||
May have the following methods:
|
||||
- requestConfirmation(session, desc_elt, client):
|
||||
- if present, it is called on when session must be accepted.
|
||||
- if it return True the session is accepted, else rejected.
|
||||
A Deferred can be returned
|
||||
- if not present, a generic accept dialog will be used
|
||||
- jingleSessionInit(self, session, content_name[, *args, **kwargs], profile): must return the domish.Element used for initial content
|
||||
- jingleHandler(self, action, session, content_name, transport_elt, profile):
|
||||
called on several action to negociate the application or transport
|
||||
"""
|
||||
if namespace in self._applications:
|
||||
raise exceptions.ConflictError(u"Trying to register already registered namespace {}".format(namespace))
|
||||
self._applications[namespace] = handler
|
||||
|
||||
def registerTransport(self, namespace, transport_type, handler, priority=0):
|
||||
"""Register a transport plugin
|
||||
|
||||
@param namespace(unicode): the XML namespace used for this transport
|
||||
@param transport_type(unicode): type of transport to use (see XEP-0166 §8)
|
||||
@param handler(object): instance of a class which manage the application.
|
||||
Must have the following methods:
|
||||
- jingleSessionInit(self, session, content_name[, *args, **kwargs], profile): must return the domish.Element used for initial content
|
||||
- jingleHandler(self, action, session, content_name, transport_elt, profile):
|
||||
called on several action to negociate the application or transport
|
||||
@param priority(int): priority of this transport
|
||||
"""
|
||||
assert transport_type in (XEP_0166.TRANSPORT_DATAGRAM, XEP_0166.TRANSPORT_STREAMING)
|
||||
if namespace in self._transports:
|
||||
raise exceptions.ConflictError(u"Trying to register already registered namespace {}".format(namespace))
|
||||
transport_data = TransportData(namespace=namespace, handler=handler, priority=priority)
|
||||
self._type_transports[transport_type].append(transport_data)
|
||||
self._type_transports[transport_type].sort(key=lambda transport_data: transport_data.priority)
|
||||
self._transports[namespace] = transport_data
|
||||
log.debug(u"new jingle transport registered")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def initiate(self, to_jid, contents, profile=C.PROF_KEY_NONE):
|
||||
"""Send a session initiation request
|
||||
|
||||
@param to_jid(jid.JID): jid to establith session with
|
||||
@param contents(list[dict]): list of contents to use:
|
||||
The dict must have the following keys:
|
||||
- app_ns(unicode): namespace of the application
|
||||
the following keys are optional:
|
||||
- transport_type(unicode): type of transport to use (see XEP-0166 §8)
|
||||
default to TRANSPORT_STREAMING
|
||||
- name(unicode): name of the content
|
||||
- app_args(list): args to pass to the application plugin
|
||||
- app_kwargs(dict): keyword args to pass to the application plugin
|
||||
@param profile: %(doc_profile)s
|
||||
"""
|
||||
assert contents # there must be at least one content
|
||||
client = self.host.getClient(profile)
|
||||
initiator = client.jid
|
||||
sid = unicode(uuid.uuid4())
|
||||
# TODO: session cleaning after timeout ?
|
||||
session = client.jingle_sessions[sid] = {'id': sid,
|
||||
'state': STATE_PENDING,
|
||||
'initiator': initiator,
|
||||
'to_jid': to_jid,
|
||||
'started': time.time(),
|
||||
'contents': {}
|
||||
}
|
||||
iq_elt, jingle_elt = self._buildJingleElt(client, session, XEP_0166.A_SESSION_INITIATE)
|
||||
jingle_elt["initiator"] = initiator.full()
|
||||
|
||||
contents_dict = session['contents']
|
||||
|
||||
for content in contents:
|
||||
# we get the application plugin
|
||||
app_ns = content['app_ns']
|
||||
try:
|
||||
application_handler = self._applications[app_ns]
|
||||
except KeyError:
|
||||
raise exceptions.InternalError(u"No application registered for {}".format(app_ns))
|
||||
|
||||
# and the transport plugin
|
||||
transport_type = content.get('transport_type', XEP_0166.TRANSPORT_STREAMING)
|
||||
try:
|
||||
transport_handler = self._type_transports[transport_type][0].handler
|
||||
except IndexError:
|
||||
raise exceptions.InternalError(u"No transport registered for {}".format(transport_type))
|
||||
|
||||
# we build the session data
|
||||
content_data = {'application': application_handler,
|
||||
'transport': transport_handler,
|
||||
'creator': INITIATOR,
|
||||
}
|
||||
try:
|
||||
content_name = content['name']
|
||||
except KeyError:
|
||||
content_name = unicode(uuid.uuid4())
|
||||
else:
|
||||
if content_name in contents_dict:
|
||||
raise exceptions.InternalError('There is already a content with this name')
|
||||
contents_dict[content_name] = content_data
|
||||
|
||||
# we construct the content element
|
||||
content_elt = jingle_elt.addElement('content')
|
||||
content_elt['creator'] = content_data['creator']
|
||||
content_elt['name'] = content_name
|
||||
|
||||
# then the description element
|
||||
app_args = content.get('app_args', [])
|
||||
app_kwargs = content.get('app_kwargs', {})
|
||||
app_kwargs['profile'] = profile
|
||||
desc_elt = yield application_handler.jingleSessionInit(session, content_name, *app_args, **app_kwargs)
|
||||
content_elt.addChild(desc_elt)
|
||||
|
||||
# and the transport one
|
||||
transport_elt = yield transport_handler.jingleSessionInit(session, content_name, profile)
|
||||
content_elt.addChild(transport_elt)
|
||||
|
||||
d = iq_elt.send()
|
||||
d.addErrback(self._iqError, sid, client)
|
||||
yield d
|
||||
|
||||
def contentTerminate(self, session, content_name, reason=REASON_SUCCESS, profile=C.PROF_KEY_NONE):
|
||||
"""Terminate and remove a content
|
||||
|
||||
if there is no more content, then session is terminated
|
||||
@param session(dict): jingle session
|
||||
@param content_name(unicode): name of the content terminated
|
||||
@param reason(unicode): reason of the termination
|
||||
@param profile: %(doc_profile)s
|
||||
"""
|
||||
contents = session['contents']
|
||||
del contents[content_name]
|
||||
if not contents:
|
||||
self.terminate(reason, session, profile)
|
||||
|
||||
## defaults methods called when plugin doesn't have them ##
|
||||
|
||||
def jingleRequestConfirmationDefault(self, session, desc_elt, profile):
|
||||
"""This method request confirmation for a jingle session"""
|
||||
log.debug(u"Using generic jingle confirmation method")
|
||||
return xml_tools.deferConfirm(self.host, _(CONFIRM_TXT).format(entity=session['to_jid'].full()), _('Confirm Jingle session'), profile=profile)
|
||||
|
||||
## jingle events ##
|
||||
|
||||
def _onJingleRequest(self, request, profile):
|
||||
"""Called when any jingle request is received
|
||||
|
||||
The request will the be dispatched to appropriate method
|
||||
according to current state
|
||||
@param request(domish.Element): received IQ request
|
||||
@para profile: %(doc_profile)s
|
||||
"""
|
||||
client = self.host.getClient(profile)
|
||||
request.handled = True
|
||||
jingle_elt = request.elements(NS_JINGLE, 'jingle').next()
|
||||
|
||||
# first we need the session id
|
||||
try:
|
||||
sid = jingle_elt['sid']
|
||||
if not sid:
|
||||
raise KeyError
|
||||
except KeyError:
|
||||
log.warning(u"Received jingle request has no sid attribute")
|
||||
self.sendError('bad-request', None, request, profile)
|
||||
return
|
||||
|
||||
# then the action
|
||||
try:
|
||||
action = jingle_elt['action']
|
||||
if not action:
|
||||
raise KeyError
|
||||
except KeyError:
|
||||
log.warning(u"Received jingle request has no action")
|
||||
self.sendError('bad-request', None, request, profile)
|
||||
return
|
||||
|
||||
to_jid = jid.JID(request['from'])
|
||||
|
||||
# we get or create the session
|
||||
try:
|
||||
session = client.jingle_sessions[sid]
|
||||
except KeyError:
|
||||
session = client.jingle_sessions[sid] = {'id': sid,
|
||||
'state': STATE_PENDING,
|
||||
'initiator': to_jid,
|
||||
'to_jid': to_jid,
|
||||
'started': time.time(),
|
||||
}
|
||||
else:
|
||||
if session['to_jid'] != to_jid:
|
||||
log.warning(u"sid conflict ({}), the jid doesn't match. Can be a collision, a hack attempt, or a bad sid generation".format(sid))
|
||||
self.sendError('service-unavailable', sid, request, profile)
|
||||
return
|
||||
if session['id'] != sid:
|
||||
log.error(u"session id doesn't match")
|
||||
self.sendError('service-unavailable', sid, request, profile)
|
||||
raise exceptions.InternalError
|
||||
|
||||
if action == XEP_0166.A_SESSION_INITIATE:
|
||||
self.onSessionInitiate(client, request, jingle_elt, session)
|
||||
elif action == XEP_0166.A_SESSION_TERMINATE:
|
||||
self.onSessionTerminate(client, request, jingle_elt, session)
|
||||
elif action == XEP_0166.A_SESSION_ACCEPT:
|
||||
self.onSessionAccept(client, request, jingle_elt, session)
|
||||
else:
|
||||
raise exceptions.InternalError(u"Unknown action {}".format(session['state']))
|
||||
|
||||
## Actions callbacks ##
|
||||
|
||||
def _parseElements(self, jingle_elt, session, request, client, new=False, creator=INITIATOR):
|
||||
"""Parse contents elements and fill contents_dict accordingly
|
||||
|
||||
after the parsing, contents_dict will containt handlers, "desc_elt" and "transport_elt"
|
||||
@param jingle_elt(domish.Element): parent <jingle> element, containing one or more <content>
|
||||
@param contents_dict(dict): session data for contents, the key is the name of the content
|
||||
@param new(bool): if new the content is new and must be created,
|
||||
else the content must exists, and session data will be filled
|
||||
@param creator(unicode): only used if new is True: creating pear (see § 7.3)
|
||||
@raise exceptions.CancelError: the error is treated the calling method can cancel the treatment (i.e. return)
|
||||
"""
|
||||
contents_dict = session['contents']
|
||||
content_elts = jingle_elt.elements(NS_JINGLE, 'content')
|
||||
|
||||
for content_elt in content_elts:
|
||||
name = content_elt['name']
|
||||
|
||||
if new:
|
||||
# the content must not exist, we check it
|
||||
if not name or name in contents_dict:
|
||||
self.sendError('bad-request', session['id'], request, client.profile)
|
||||
raise exceptions.CancelError
|
||||
content_data = contents_dict[name] = {'creator': creator}
|
||||
else:
|
||||
# the content must exist, we check it
|
||||
try:
|
||||
content_data = contents_dict[name]
|
||||
except KeyError:
|
||||
log.warning(u"Other peer try to access an unknown content")
|
||||
self.sendError('bad-request', session['id'], request, client.profile)
|
||||
raise exceptions.CancelError
|
||||
|
||||
# application
|
||||
desc_elt = content_elt.description
|
||||
if not desc_elt:
|
||||
self.sendError('bad-request', session['id'], request, client.profile)
|
||||
raise exceptions.CancelError
|
||||
|
||||
if new:
|
||||
# the content is new, we need to check and link the application_handler
|
||||
app_ns = desc_elt.uri
|
||||
if not app_ns or app_ns == NS_JINGLE:
|
||||
self.sendError('bad-request', session['id'], request, client.profile)
|
||||
raise exceptions.CancelError
|
||||
|
||||
try:
|
||||
application_handler = self._applications[app_ns]
|
||||
except KeyError:
|
||||
log.warning(u"Unmanaged application namespace [{}]".format(app_ns))
|
||||
self.sendError('service-unavailable', session['id'], request, client.profile)
|
||||
raise exceptions.CancelError
|
||||
|
||||
content_data['application'] = application_handler
|
||||
else:
|
||||
# the content exists, we check that we have not a former desc_elt
|
||||
if 'desc_elt' in content_data:
|
||||
raise exceptions.InternalError(u"desc_elt should not exist at this point")
|
||||
|
||||
content_data['desc_elt'] = desc_elt
|
||||
|
||||
# transport
|
||||
transport_elt = content_elt.transport
|
||||
if not transport_elt:
|
||||
self.sendError('bad-request', session['id'], request, client.profile)
|
||||
raise exceptions.CancelError
|
||||
|
||||
if new:
|
||||
# the content is new, we need to check and link the transport_handler
|
||||
transport_ns = transport_elt.uri
|
||||
if not app_ns or app_ns == NS_JINGLE:
|
||||
self.sendError('bad-request', session['id'], request, client.profile)
|
||||
raise exceptions.CancelError
|
||||
|
||||
try:
|
||||
transport_handler = self._transports[transport_ns].handler
|
||||
except KeyError:
|
||||
raise exceptions.InternalError(u"No transport registered for namespace {}".format(transport_ns))
|
||||
content_data['transport'] = transport_handler
|
||||
else:
|
||||
# the content exists, we check that we have not a former transport_elt
|
||||
if 'transport_elt' in content_data:
|
||||
raise exceptions.InternalError(u"desc_elt should not exist at this point")
|
||||
|
||||
content_data['transport_elt'] = transport_elt
|
||||
|
||||
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):
|
||||
"""Call application and transport plugin methods for all contents
|
||||
|
||||
@param action(unicode): jingle action name
|
||||
@param session(dict): jingle session data
|
||||
@param app_method_name(unicode, None): name of the method to call for applications
|
||||
None to ignore
|
||||
@param transp_method_name(unicode, None): name of the method to call for transports
|
||||
None to ignore
|
||||
@param app_default_cb(callable, None): default callback to use if plugin has not app_method_name
|
||||
None to raise an exception instead
|
||||
@param transp_default_cb(callable, None): default callback to use if plugin has not transp_method_name
|
||||
None to raise an exception instead
|
||||
@param delete(bool): if True, remove desc_elt and transport_elt from session
|
||||
ignored if elements is False
|
||||
@param elements(bool): True if elements(desc_elt and tranport_elt) must be managed
|
||||
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)
|
||||
@param profile(unicode): %(doc_profile)s
|
||||
@return (list[defer.Deferred]): list of launched Deferred
|
||||
"""
|
||||
contents_dict = session['contents']
|
||||
defers_list = []
|
||||
for content_name, content_data in contents_dict.iteritems():
|
||||
for method_name, handler_key, default_cb, elt_name in (
|
||||
(app_method_name, 'application', app_default_cb, 'desc_elt'),
|
||||
(transp_method_name, 'transport', transp_default_cb, 'transport_elt')):
|
||||
if method_name is None:
|
||||
continue
|
||||
|
||||
handler = content_data[handler_key]
|
||||
try:
|
||||
method = getattr(handler, method_name)
|
||||
except AttributeError:
|
||||
if default_cb is not None:
|
||||
method = default_cb
|
||||
else:
|
||||
raise exceptions.InternalError(u'{} not implemented !'.format(method_name))
|
||||
finally:
|
||||
if elements:
|
||||
elt = content_data.pop(elt_name) if delete else content_data[elt_name]
|
||||
else:
|
||||
elt = None
|
||||
d = defer.maybeDeferred(method, action, session, content_name, elt, profile)
|
||||
defers_list.append(d)
|
||||
|
||||
return defers_list
|
||||
|
||||
def onSessionInitiate(self, client, request, jingle_elt, session):
|
||||
"""Called on session-initiate action
|
||||
|
||||
The "jingleRequestConfirmation" method of each application will be called
|
||||
(or self.jingleRequestConfirmationDefault if the former doesn't exist).
|
||||
The session is only accepted if all application are confirmed.
|
||||
The application must manage itself multiple contents scenari (e.g. audio/video).
|
||||
@param client: %(doc_client)s
|
||||
@param request(domish.Element): full request
|
||||
@param jingle_elt(domish.Element): <jingle> element
|
||||
@param session(dict): session data
|
||||
"""
|
||||
if 'contents' in session:
|
||||
raise exceptions.InternalError("Contents dict should not already exist at this point")
|
||||
session['contents'] = contents_dict = {}
|
||||
|
||||
try:
|
||||
self._parseElements(jingle_elt, session, request, client, True, INITIATOR)
|
||||
except exceptions.CancelError:
|
||||
return
|
||||
|
||||
if not contents_dict:
|
||||
# there MUST be at least one content
|
||||
self.sendError('bad-request', session['id'], request, client.profile)
|
||||
return
|
||||
|
||||
# at this point we can send the <iq/> result to confirm reception of the request
|
||||
client.xmlstream.send(xmlstream.toResponse(request, 'result'))
|
||||
|
||||
# we now request each application plugin confirmation
|
||||
# and if all are accepted, we can accept the session
|
||||
confirm_defers = self._callPlugins(XEP_0166.A_SESSION_INITIATE, session, 'jingleRequestConfirmation', None, self.jingleRequestConfirmationDefault, delete=False, profile=client.profile)
|
||||
|
||||
confirm_dlist = defer.gatherResults(confirm_defers)
|
||||
confirm_dlist.addCallback(self._confirmationCb, session, jingle_elt, client)
|
||||
confirm_dlist.addErrback(self._jingleErrorCb, session['id'], request, client)
|
||||
|
||||
def _confirmationCb(self, confirm_results, session, jingle_elt, client):
|
||||
"""Method called when confirmation from user has been received
|
||||
|
||||
This method is only called for the responder
|
||||
@param confirm_results(list[bool]): all True if session is accepted
|
||||
@param session(dict): session data
|
||||
@param jingle_elt(domish.Element): jingle data of this session
|
||||
@param client: %(doc_client)s
|
||||
"""
|
||||
confirmed = all(confirm_results)
|
||||
if not confirmed:
|
||||
return self.terminate(XEP_0166.REASON_DECLINE, session, client.profile)
|
||||
|
||||
iq_elt, jingle_elt = self._buildJingleElt(client, session, XEP_0166.A_SESSION_ACCEPT)
|
||||
jingle_elt['responder'] = client.jid.full()
|
||||
|
||||
# contents
|
||||
|
||||
def addElement(domish_elt, content_elt):
|
||||
content_elt.addChild(domish_elt)
|
||||
|
||||
defers_list = []
|
||||
|
||||
for content_name, content_data in session['contents'].iteritems():
|
||||
content_elt = jingle_elt.addElement('content')
|
||||
content_elt['creator'] = INITIATOR
|
||||
content_elt['name'] = content_name
|
||||
|
||||
application_handler = content_data['application']
|
||||
app_session_accept_cb = application_handler.jingleHandler
|
||||
|
||||
app_d = defer.maybeDeferred(app_session_accept_cb,
|
||||
XEP_0166.A_SESSION_INITIATE, session, content_name, content_data.pop('desc_elt'), client.profile)
|
||||
app_d.addCallback(addElement, content_elt)
|
||||
defers_list.append(app_d)
|
||||
|
||||
transport_handler = content_data['transport']
|
||||
transport_session_accept_cb = transport_handler.jingleHandler
|
||||
|
||||
transport_d = defer.maybeDeferred(transport_session_accept_cb,
|
||||
XEP_0166.A_SESSION_INITIATE, session, content_name, content_data.pop('transport_elt'), client.profile)
|
||||
transport_d.addCallback(addElement, content_elt)
|
||||
defers_list.append(transport_d)
|
||||
|
||||
d_list = defer.DeferredList(defers_list)
|
||||
d_list.addCallback(lambda dummy: self._callPlugins(XEP_0166.A_PREPARE_RESPONDER, session, app_method_name=None, elements=False, profile=client.profile))
|
||||
d_list.addCallback(lambda dummy: iq_elt.send())
|
||||
def changeState(dummy, session):
|
||||
session['state'] = STATE_ACTIVE
|
||||
|
||||
d_list.addCallback(changeState, session)
|
||||
d_list.addCallback(lambda dummy: self._callPlugins(XEP_0166.A_ACCEPTED_ACK, session, elements=False, profile=client.profile))
|
||||
d_list.addErrback(self._iqError, session['id'], client)
|
||||
return d_list
|
||||
|
||||
def onSessionTerminate(self, client, request, jingle_elt, session):
|
||||
# TODO: check reason, display a message to user if needed
|
||||
log.debug("Jingle Session {} terminated".format(session['id']))
|
||||
self._delSession(client, session['id'])
|
||||
client.xmlstream.send(xmlstream.toResponse(request, 'result'))
|
||||
|
||||
def onSessionAccept(self, client, request, jingle_elt, session):
|
||||
"""Method called one sesion is accepted
|
||||
|
||||
This method is only called for initiator
|
||||
@param client: %(doc_client)s
|
||||
@param request(domish.Element): full <iq> request
|
||||
@param jingle_elt(domish.Element): the <jingle> element
|
||||
@param session(dict): session data
|
||||
"""
|
||||
log.debug(u"Jingle session {} has been accepted".format(session['id']))
|
||||
|
||||
try:
|
||||
self._parseElements(jingle_elt, session, request, client)
|
||||
except exceptions.CancelError:
|
||||
return
|
||||
|
||||
# at this point we can send the <iq/> result to confirm reception of the request
|
||||
client.xmlstream.send(xmlstream.toResponse(request, 'result'))
|
||||
# and change the state
|
||||
session['state'] = STATE_ACTIVE
|
||||
|
||||
negociate_defers = []
|
||||
negociate_defers = self._callPlugins(XEP_0166.A_SESSION_ACCEPT, session, profile=client.profile)
|
||||
|
||||
negociate_dlist = defer.DeferredList(negociate_defers)
|
||||
|
||||
# after negociations we start the transfer
|
||||
negociate_dlist.addCallback(lambda dummy: self._callPlugins(XEP_0166.A_START, session, app_method_name=None, elements=False, profile=client.profile))
|
||||
|
||||
|
||||
class XEP_0166_handler(xmlstream.XMPPHandler):
|
||||
implements(iwokkel.IDisco)
|
||||
|
||||
def __init__(self, plugin_parent):
|
||||
self.plugin_parent = plugin_parent
|
||||
|
||||
def connectionInitialized(self):
|
||||
self.xmlstream.addObserver(JINGLE_REQUEST, self.plugin_parent._onJingleRequest, profile=self.parent.profile)
|
||||
|
||||
def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
|
||||
return [disco.DiscoFeature(NS_JINGLE)]
|
||||
|
||||
def getDiscoItems(self, requestor, target, nodeIdentifier=''):
|
||||
return []
|
||||
Loading…
Reference in a new issue