Mercurial > libervia-backend
diff src/plugins/plugin_xep_0166.py @ 2489:e2a7bb875957
plugin pipe/stream, file transfert: refactoring and improvments:
this is a big patch as things had to be changed at the same time.
- changed methods using profile argument to use client instead
- move SatFile in a new tools.stream module, has it should be part of core, not a plugin
- new IStreamProducer interface, to handler starting a pull producer
- new FileStreamObject which create a stream producer/consumer from a SatFile
- plugin pipe is no more using unix named pipe, as it complicate the thing,
special care need to be taken to not block, and it's generally not necessary.
Instead a socket is now used, so the plugin has been renomed to jingle stream.
- bad connection/error should be better handler in jingle stream plugin, and code should not block anymore
- jp pipe commands have been updated accordingly
fix bug 237
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 08 Feb 2018 00:37:42 +0100 |
parents | ed1d71b91b29 |
children | 7ad5f2c4e34a |
line wrap: on
line diff
--- a/src/plugins/plugin_xep_0166.py Thu Feb 01 07:24:34 2018 +0100 +++ b/src/plugins/plugin_xep_0166.py Thu Feb 08 00:37:42 2018 +0100 @@ -122,16 +122,14 @@ jingle_elt['action'] = action return iq_elt, jingle_elt - def sendError(self, error_condition, sid, request, jingle_condition=None, profile=C.PROF_KEY_NONE): + def sendError(self, client, error_condition, sid, request, jingle_condition=None): """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 jingle_condition(None, unicode): if not None, additional jingle-specific error information - @param profile: %(doc_profile)s """ - client = self.host.getClient(profile) iq_elt = error.StanzaError(error_condition).toResponse(request) if jingle_condition is not None: iq_elt.error.addElement((NS_JINGLE_ERROR, jingle_condition)) @@ -143,16 +141,14 @@ def _terminateEb(self, failure_): log.warning(_(u"Error while terminating session: {msg}").format(msg=failure_)) - def terminate(self, reason, session, profile): + def terminate(self, client, reason, session): """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): @@ -172,7 +168,6 @@ @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) @@ -189,7 +184,7 @@ """ log.warning("Error while processing jingle request") if isinstance(fail, exceptions.DataError): - self.sendError('bad-request', sid, request, profile=client.profile) + self.sendError(client, 'bad-request', sid, request) else: log.error("Unmanaged jingle exception") self._delSession(client, sid) @@ -208,8 +203,8 @@ - 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): + - jingleSessionInit(client, self, session, content_name[, *args, **kwargs]): must return the domish.Element used for initial content + - jingleHandler(client, self, action, session, content_name, transport_elt): called on several action to negociate the application or transport - jingleTerminate: called on session terminate, with reason_elt May be used to clean session @@ -226,8 +221,8 @@ @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): + - jingleSessionInit(client, self, session, content_name[, *args, **kwargs]): must return the domish.Element used for initial content + - jingleHandler(client, self, action, session, content_name, transport_elt): called on several action to negociate the application or transport @param priority(int): priority of this transport """ @@ -241,25 +236,23 @@ log.debug(u"new jingle transport registered") @defer.inlineCallbacks - def transportReplace(self, transport_ns, session, content_name, profile=C.PROF_KEY_NONE): + def transportReplace(self, client, transport_ns, session, content_name): """Replace a transport @param transport_ns(unicode): namespace of the new transport to use @param session(dict): jingle session data @param content_name(unicode): name of the content - @param profile: %(doc_profile)s """ # XXX: for now we replace the transport before receiving confirmation from other peer # this is acceptable because we terminate the session if transport is rejected. # this behavious may change in the future. - client = self.host.getClient(profile) content_data= session['contents'][content_name] transport_data = content_data['transport_data'] try: transport = self._transports[transport_ns] except KeyError: raise exceptions.InternalError(u"Unkown transport") - yield content_data['transport'].handler.jingleHandler(XEP_0166.A_DESTROY, session, content_name, None, profile) + yield content_data['transport'].handler.jingleHandler(client, XEP_0166.A_DESTROY, session, content_name, None) content_data['transport'] = transport transport_data.clear() @@ -268,11 +261,11 @@ content_elt['name'] = content_name content_elt['creator'] = content_data['creator'] - transport_elt = transport.handler.jingleSessionInit(session, content_name, profile) + transport_elt = transport.handler.jingleSessionInit(client, session, content_name) content_elt.addChild(transport_elt) iq_elt.send() - def buildAction(self, action, session, content_name, profile=C.PROF_KEY_NONE): + def buildAction(self, client, action, session, content_name): """Build an element according to requested action @param action(unicode): a jingle action (see XEP-0166 §7.2), @@ -280,11 +273,8 @@ transport-replace is managed in the dedicated [transportReplace] method @param session(dict): jingle session data @param content_name(unicode): name of the content - @param profile: %(doc_profile)s @return (tuple[domish.Element, domish.Element]): parent <iq> element, <transport> or <description> element, according to action """ - client = self.host.getClient(profile) - # we first build iq, jingle and content element which are the same in every cases iq_elt, jingle_elt = self._buildJingleElt(client, session, action) # FIXME: XEP-0260 § 2.3 Ex 5 has an initiator attribute, but it should not according to XEP-0166 §7.1 table 1, must be checked @@ -301,18 +291,16 @@ return iq_elt, context_elt - def buildSessionInfo(self, session, profile=C.PROF_KEY_NONE): + def buildSessionInfo(self, client, session): """Build a session-info action @param session(dict): jingle session data - @param profile: %(doc_profile)s @return (tuple[domish.Element, domish.Element]): parent <iq> element, <jingle> element """ - client = self.host.getClient(profile) return self._buildJingleElt(client, session, XEP_0166.A_SESSION_INFO) @defer.inlineCallbacks - def initiate(self, peer_jid, contents, profile=C.PROF_KEY_NONE): + def initiate(self, client, peer_jid, contents): """Send a session initiation request @param peer_jid(jid.JID): jid to establith session with @@ -327,11 +315,9 @@ default to BOTH (see XEP-0166 §7.3) - 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 @return D(unicode): jingle session id """ 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 ? @@ -392,12 +378,11 @@ # 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) + desc_elt = yield application.handler.jingleSessionInit(client, 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) + transport_elt = yield transport.handler.jingleSessionInit(client, session, content_name) content_elt.addChild(transport_elt) d = iq_elt.send() @@ -411,38 +396,35 @@ """ reactor.callLater(0, self.contentTerminate, *args, **kwargs) - def contentTerminate(self, session, content_name, reason=REASON_SUCCESS, profile=C.PROF_KEY_NONE): + def contentTerminate(self, client, session, content_name, reason=REASON_SUCCESS): """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) + self.terminate(client, reason, session) ## defaults methods called when plugin doesn't have them ## - def jingleRequestConfirmationDefault(self, action, session, content_name, desc_elt, profile): + def jingleRequestConfirmationDefault(self, client, action, session, content_name, desc_elt): """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['peer_jid'].full()), _('Confirm Jingle session'), profile=profile) + return xml_tools.deferConfirm(self.host, _(CONFIRM_TXT).format(entity=session['peer_jid'].full()), _('Confirm Jingle session'), profile=client.profile) ## jingle events ## - def _onJingleRequest(self, request, profile): + def _onJingleRequest(self, request, client): """Called when any jingle request is received - The request will the be dispatched to appropriate method + The request will then 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() @@ -453,7 +435,7 @@ raise KeyError except KeyError: log.warning(u"Received jingle request has no sid attribute") - self.sendError('bad-request', None, request, profile=profile) + self.sendError(client, 'bad-request', None, request) return # then the action @@ -463,7 +445,7 @@ raise KeyError except KeyError: log.warning(u"Received jingle request has no action") - self.sendError('bad-request', None, request, profile=profile) + self.sendError(client, 'bad-request', None, request) return peer_jid = jid.JID(request['from']) @@ -472,9 +454,18 @@ try: session = client.jingle_sessions[sid] except KeyError: - if action != XEP_0166.A_SESSION_INITIATE: - log.warning(u"Received request for an unknown session id: {}".format(sid)) - self.sendError('item-not-found', None, request, 'unknown-session', profile=profile) + if action == XEP_0166.A_SESSION_INITIATE: + pass + elif action == XEP_0166.A_SESSION_TERMINATE: + log.debug(u"ignoring session terminate action (inexisting session id): {request_id} [{profile}]".format( + request_id=sid, + profile = client.profile)) + return + else: + log.warning(u"Received request for an unknown session id: {request_id} [{profile}]".format( + request_id=sid, + profile = client.profile)) + self.sendError(client, 'item-not-found', None, request, 'unknown-session') return session = client.jingle_sessions[sid] = {'id': sid, @@ -487,11 +478,11 @@ else: if session['peer_jid'] != peer_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=profile) + self.sendError(client, 'service-unavailable', sid, request) return if session['id'] != sid: log.error(u"session id doesn't match") - self.sendError('service-unavailable', sid, request, profile=profile) + self.sendError(client, 'service-unavailable', sid, request) raise exceptions.InternalError if action == XEP_0166.A_SESSION_INITIATE: @@ -539,7 +530,7 @@ 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, profile=client.profile) + self.sendError(client, 'bad-request', session['id'], request) raise exceptions.CancelError content_data = contents_dict[name] = {'creator': creator, 'senders': content_elt.attributes.get('senders', 'both'), @@ -550,28 +541,28 @@ 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, profile=client.profile) + self.sendError(client, 'bad-request', session['id'], request) raise exceptions.CancelError # application if with_application: desc_elt = content_elt.description if not desc_elt: - self.sendError('bad-request', session['id'], request, profile=client.profile) + self.sendError(client, 'bad-request', session['id'], request) raise exceptions.CancelError if new: # the content is new, we need to check and link the application app_ns = desc_elt.uri if not app_ns or app_ns == NS_JINGLE: - self.sendError('bad-request', session['id'], request, profile=client.profile) + self.sendError(client, 'bad-request', session['id'], request) raise exceptions.CancelError try: application = self._applications[app_ns] except KeyError: log.warning(u"Unmanaged application namespace [{}]".format(app_ns)) - self.sendError('service-unavailable', session['id'], request, profile=client.profile) + self.sendError(client, 'service-unavailable', session['id'], request) raise exceptions.CancelError content_data['application'] = application @@ -587,14 +578,14 @@ if with_transport: transport_elt = content_elt.transport if not transport_elt: - self.sendError('bad-request', session['id'], request, profile=client.profile) + self.sendError(client, 'bad-request', session['id'], request) raise exceptions.CancelError if new: # the content is new, we need to check and link the transport transport_ns = transport_elt.uri if not app_ns or app_ns == NS_JINGLE: - self.sendError('bad-request', session['id'], request, profile=client.profile) + self.sendError(client, 'bad-request', session['id'], request) raise exceptions.CancelError try: @@ -610,16 +601,17 @@ content_data['transport_elt'] = transport_elt - def _ignore(self, action, session, content_name, elt, profile): + def _ignore(self, client, action, session, content_name, elt): """Dummy method used when not exception must be raised if a method is not implemented in _callPlugins must be used as app_default_cb and/or transp_default_cb """ return elt - def _callPlugins(self, action, session, app_method_name='jingleHandler', transp_method_name='jingleHandler', + def _callPlugins(self, client, action, session, app_method_name='jingleHandler', + transp_method_name='jingleHandler', app_default_cb=None, transp_default_cb=None, delete=True, - elements=True, force_element=None, profile=C.PROF_KEY_NONE): + elements=True, force_element=None): """Call application and transport plugin methods for all contents @param action(unicode): jingle action name @@ -635,10 +627,10 @@ @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 used in a request, and False if it used after a request (i.e. on <iq> result or error) + must be True if _callPlugins is used in a request, and False if it used after a request + (i.e. on <iq> result or error) @param force_element(None, domish.Element, object): if elements is False, it is used as element parameter else it is ignored - @param profile(unicode): %(doc_profile)s @return (list[defer.Deferred]): list of launched Deferred @raise exceptions.NotFound: method is not implemented """ @@ -663,7 +655,7 @@ elt = content_data.pop(elt_name) if delete else content_data[elt_name] else: elt = force_element - d = defer.maybeDeferred(method, action, session, content_name, elt, profile) + d = defer.maybeDeferred(method, client, action, session, content_name, elt) defers_list.append(d) return defers_list @@ -691,7 +683,7 @@ if not contents_dict: # there MUST be at least one content - self.sendError('bad-request', session['id'], request, profile=client.profile) + self.sendError(client, 'bad-request', session['id'], request) return # at this point we can send the <iq/> result to confirm reception of the request @@ -699,7 +691,7 @@ # 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_defers = self._callPlugins(client, XEP_0166.A_SESSION_INITIATE, session, 'jingleRequestConfirmation', None, self.jingleRequestConfirmationDefault, delete=False) confirm_dlist = defer.gatherResults(confirm_defers) confirm_dlist.addCallback(self._confirmationCb, session, jingle_elt, client) @@ -716,7 +708,7 @@ """ confirmed = all(confirm_results) if not confirmed: - return self.terminate(XEP_0166.REASON_DECLINE, session, client.profile) + return self.terminate(client, XEP_0166.REASON_DECLINE, session) iq_elt, jingle_elt = self._buildJingleElt(client, session, XEP_0166.A_SESSION_ACCEPT) jingle_elt['responder'] = client.jid.full() @@ -736,27 +728,27 @@ application = 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 = defer.maybeDeferred(app_session_accept_cb, client, + XEP_0166.A_SESSION_INITIATE, session, content_name, content_data.pop('desc_elt')) app_d.addCallback(addElement, content_elt) defers_list.append(app_d) transport = 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 = defer.maybeDeferred(transport_session_accept_cb, client, + XEP_0166.A_SESSION_INITIATE, session, content_name, content_data.pop('transport_elt')) 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: self._callPlugins(client, XEP_0166.A_PREPARE_RESPONDER, session, app_method_name=None, elements=False)) 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.addCallback(lambda dummy: self._callPlugins(client, XEP_0166.A_ACCEPTED_ACK, session, elements=False)) d_list.addErrback(self._iqError, session['id'], client) return d_list @@ -769,7 +761,7 @@ log.warning(u"Not reason given for session termination") reason_elt = jingle_elt.addElement('reason') - terminate_defers = self._callPlugins(XEP_0166.A_SESSION_TERMINATE, session, 'jingleTerminate', 'jingleTerminate', self._ignore, self._ignore, elements=False, force_element=reason_elt, profile=client.profile) + terminate_defers = self._callPlugins(client, XEP_0166.A_SESSION_TERMINATE, session, 'jingleTerminate', 'jingleTerminate', self._ignore, self._ignore, elements=False, force_element=reason_elt) terminate_dlist = defer.DeferredList(terminate_defers) terminate_dlist.addCallback(lambda dummy: self._delSession(client, session['id'])) @@ -797,12 +789,12 @@ session['state'] = STATE_ACTIVE negociate_defers = [] - negociate_defers = self._callPlugins(XEP_0166.A_SESSION_ACCEPT, session, profile=client.profile) + negociate_defers = self._callPlugins(client, XEP_0166.A_SESSION_ACCEPT, session) 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)) + negociate_dlist.addCallback(lambda dummy: self._callPlugins(client, XEP_0166.A_START, session, app_method_name=None, elements=False)) def _onSessionCb(self, result, client, request, jingle_elt, session): client.send(xmlstream.toResponse(request, 'result')) @@ -810,7 +802,7 @@ def _onSessionEb(self, failure_, client, request, jingle_elt, session): log.error(u"Error while handling onSessionInfo: {}".format(failure_.value)) # XXX: only error managed so far, maybe some applications/transports need more - self.sendError('feature-not-implemented', None, request, 'unsupported-info', client.profile) + self.sendError(client, 'feature-not-implemented', None, request, 'unsupported-info') def onSessionInfo(self, client, request, jingle_elt, session): """Method called when a session-info action is received from other peer @@ -829,8 +821,8 @@ try: # XXX: session-info is most likely only used for application, so we don't call transport plugins # if a future transport use it, this behaviour must be adapted - defers = self._callPlugins(XEP_0166.A_SESSION_INFO, session, 'jingleSessionInfo', None, - elements=False, force_element=jingle_elt, profile=client.profile) + defers = self._callPlugins(client, XEP_0166.A_SESSION_INFO, session, 'jingleSessionInfo', None, + elements=False, force_element=jingle_elt) except exceptions.NotFound as e: self._onSessionEb(failure.Failure(e), client, request, jingle_elt, session) return @@ -888,7 +880,7 @@ iq_elt, accept_jingle_elt = self._buildJingleElt(client, session, XEP_0166.A_TRANSPORT_ACCEPT) for content_name, content_data, transport, transport_elt in to_replace: # we can now actually replace the transport - yield content_data['transport'].handler.jingleHandler(XEP_0166.A_DESTROY, session, content_name, None, client.profile) + yield content_data['transport'].handler.jingleHandler(client, XEP_0166.A_DESTROY, session, content_name, None) content_data['transport'] = transport content_data['transport_data'].clear() # and build the element @@ -896,10 +888,10 @@ content_elt['name'] = content_name content_elt['creator'] = content_data['creator'] # we notify the transport and insert its <transport/> in the answer - accept_transport_elt = yield transport.handler.jingleHandler(XEP_0166.A_TRANSPORT_REPLACE, session, content_name, transport_elt, client.profile) + accept_transport_elt = yield transport.handler.jingleHandler(client, XEP_0166.A_TRANSPORT_REPLACE, session, content_name, transport_elt) content_elt.addChild(accept_transport_elt) # there is no confirmation needed here, so we can directly prepare it - yield transport.handler.jingleHandler(XEP_0166.A_PREPARE_RESPONDER, session, content_name, None, client.profile) + yield transport.handler.jingleHandler(client, XEP_0166.A_PREPARE_RESPONDER, session, content_name, None) iq_elt.send() @@ -922,12 +914,12 @@ client.send(xmlstream.toResponse(request, 'result')) negociate_defers = [] - negociate_defers = self._callPlugins(XEP_0166.A_TRANSPORT_ACCEPT, session, app_method_name=None, profile=client.profile) + negociate_defers = self._callPlugins(client, XEP_0166.A_TRANSPORT_ACCEPT, session, app_method_name=None) 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)) + negociate_dlist.addCallback(lambda dummy: self._callPlugins(client, XEP_0166.A_START, session, app_method_name=None, elements=False)) def onTransportReject(self, client, request, jingle_elt, session): """Method called when a transport replacement is refused @@ -939,7 +931,7 @@ """ # XXX: for now, we terminate the session in case of transport-reject # this behaviour may change in the future - self.terminate('failed-transport', session, client.profile) + self.terminate(client, 'failed-transport', session) def onTransportInfo(self, client, request, jingle_elt, session): """Method called when a transport-info action is received from other peer @@ -966,7 +958,7 @@ except KeyError: continue else: - content_data['transport'].handler.jingleHandler(XEP_0166.A_TRANSPORT_INFO, session, content_name, transport_elt, client.profile) + content_data['transport'].handler.jingleHandler(client, XEP_0166.A_TRANSPORT_INFO, session, content_name, transport_elt) class XEP_0166_handler(xmlstream.XMPPHandler): @@ -976,7 +968,7 @@ self.plugin_parent = plugin_parent def connectionInitialized(self): - self.xmlstream.addObserver(JINGLE_REQUEST, self.plugin_parent._onJingleRequest, profile=self.parent.profile) + self.xmlstream.addObserver(JINGLE_REQUEST, self.plugin_parent._onJingleRequest, client=self.parent) def getDiscoInfo(self, requestor, target, nodeIdentifier=''): return [disco.DiscoFeature(NS_JINGLE)]