# HG changeset patch # User Goffi # Date 1613746222 -3600 # Node ID 12dc234f698cdb6eb0b8197269e70c444431c8f3 # Parent 02a8d227d5bb40f971b222ebd1ece7d9f01a350f plugin invitation: pubsub invitations: - new Pubsub invitation plugin, to have a generic way to manage invitation on Pubsub based features - `invitePreflight` and `onInvitationPreflight` method can be implemented to customise invitation for a namespace - refactored events invitations to use the new plugin - a Pubsub invitation can now be for a whole node instead of a specific item - if invitation is for a node, a namespace can be specified to indicate what this node is about. It is then added in `extra` data - an element (domish.Element) can be added in `extra` data, it will then be added in the invitation - some code modernisation diff -r 02a8d227d5bb -r 12dc234f698c sat/plugins/plugin_exp_events.py --- a/sat/plugins/plugin_exp_events.py Fri Feb 19 15:49:59 2021 +0100 +++ b/sat/plugins/plugin_exp_events.py Fri Feb 19 15:50:22 2021 +0100 @@ -17,11 +17,13 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import Optional import shortuuid from sat.core.i18n import _ from sat.core import exceptions from sat.core.constants import Const as C from sat.core.log import getLogger +from sat.core.xmpp import SatXMPPEntity from sat.tools import utils from sat.tools.common import uri as xmpp_uri from sat.tools.common import date_utils @@ -41,7 +43,7 @@ C.PI_IMPORT_NAME: "EVENTS", C.PI_TYPE: "EXP", C.PI_PROTOCOLS: [], - C.PI_DEPENDENCIES: ["XEP-0060", "INVITATION", "LIST_INTEREST"], + C.PI_DEPENDENCIES: ["XEP-0060", "INVITATION", "PUBSUB_INVITATION", "LIST_INTEREST"], C.PI_RECOMMENDATIONS: ["XEP-0277", "EMAIL_INVITATION"], C.PI_MAIN: "Events", C.PI_HANDLER: "yes", @@ -61,8 +63,7 @@ self._i = self.host.plugins.get("EMAIL_INVITATION") self._b = self.host.plugins.get("XEP-0277") self.host.registerNamespace("event", NS_EVENT) - self.host.plugins["INVITATION"].registerNamespace(NS_EVENT, - self.register) + self.host.plugins["PUBSUB_INVITATION"].register(NS_EVENT, self) host.bridge.addMethod( "eventGet", ".plugin", @@ -232,32 +233,6 @@ raise exceptions.NotFound(_("No event with this id has been found")) defer.returnValue(event_elt) - def register(self, client, name, extra, service, node, event_id, item_elt, - creator=False): - """Register evenement in personal events list - - @param service(jid.JID): pubsub service of the event - @param node(unicode): event node - @param event_id(unicode): event id - @param event_elt(domish.Element): event element - note that this element will be modified in place - @param creator(bool): True if client's profile is the creator of the node - """ - event_elt = item_elt.event - link_elt = event_elt.addElement("link") - link_elt["service"] = service.full() - link_elt["node"] = node - link_elt["item"] = event_id - __, event_data = self._parseEventElt(event_elt) - name = event_data.get('name') - if 'image' in event_data: - extra = {'thumb_url': event_data['image']} - else: - extra = None - return self.host.plugins['LIST_INTEREST'].registerPubsub( - client, NS_EVENT, service, node, event_id, creator, - name=name, element=event_elt, extra=extra) - def _eventGet(self, service, node, id_="", profile_key=C.PROF_KEY_NONE): service = jid.JID(service) if service else None node = node if node else NS_EVENT @@ -289,10 +264,11 @@ node = node or None client = self.host.getClient(profile_key) data["register"] = C.bool(data.get("register", C.BOOL_FALSE)) - return self.eventCreate(client, timestamp, data, service, node, id_ or NS_EVENT) + return defer.ensureDeferred( + self.eventCreate(client, timestamp, data, service, node, id_ or NS_EVENT) + ) - @defer.inlineCallbacks - def eventCreate(self, client, timestamp, data, service, node=None, event_id=NS_EVENT): + async def eventCreate(self, client, timestamp, data, service, node=None, event_id=NS_EVENT): """Create or replace an event @param service(jid.JID, None): PubSub service @@ -341,8 +317,8 @@ del data[k] if key not in data: # FIXME: affiliate invitees - uri_node = yield self._p.createNode(client, service) - yield self._p.setConfiguration( + uri_node = await self._p.createNode(client, service) + await self._p.setConfiguration( client, service, uri_node, @@ -372,17 +348,23 @@ item_elt = pubsub.Item(id=event_id, payload=event_elt) try: # TODO: check auto-create, no need to create node first if available - node = yield self._p.createNode(client, service, nodeIdentifier=node) + node = await self._p.createNode(client, service, nodeIdentifier=node) except error.StanzaError as e: if e.condition == "conflict": log.debug(_("requested node already exists")) - yield self._p.publish(client, service, node, items=[item_elt]) + await self._p.publish(client, service, node, items=[item_elt]) if register: - yield self.register( - client, None, {}, service, node, event_id, item_elt, creator=True) - defer.returnValue(node) + extra = {} + self.onInvitationPreflight( + client, "", extra, service, node, event_id, item_elt + ) + await self.host.plugins['LIST_INTEREST'].registerPubsub( + client, NS_EVENT, service, node, event_id, True, + extra.pop("name", ""), extra.pop("element"), extra + ) + return node def _eventModify(self, service, node, id_, timestamp_update, data_update, profile_key=C.PROF_KEY_NONE): @@ -390,12 +372,14 @@ if not node: raise ValueError(_("missing node")) client = self.host.getClient(profile_key) - return self.eventModify( - client, service, node, id_ or NS_EVENT, timestamp_update or None, data_update + return defer.ensureDeferred( + self.eventModify( + client, service, node, id_ or NS_EVENT, timestamp_update or None, + data_update + ) ) - @defer.inlineCallbacks - def eventModify( + async def eventModify( self, client, service, node, id_=NS_EVENT, timestamp_update=None, data_update=None ): """Update an event @@ -403,13 +387,13 @@ Similar as create instead that it update existing item instead of creating or replacing it. Params are the same as for [eventCreate]. """ - event_timestamp, event_metadata = yield self.eventGet(client, service, node, id_) + event_timestamp, event_metadata = await self.eventGet(client, service, node, id_) new_timestamp = event_timestamp if timestamp_update is None else timestamp_update new_data = event_metadata if data_update: for k, v in data_update.items(): new_data[k] = v - yield self.eventCreate(client, new_timestamp, new_data, service, node, id_) + await self.eventCreate(client, new_timestamp, new_data, service, node, id_) def _eventsListSerialise(self, events): for timestamp, data in events: @@ -543,52 +527,40 @@ invitees[item["id"]] = data defer.returnValue(invitees) - def _invite(self, invitee_jid, service, node, item_id, profile): - client = self.host.getClient(profile) - service = jid.JID(service) if service else None - node = node or None - item_id = item_id or None - invitee_jid = jid.JID(invitee_jid) - return self.invite(client, invitee_jid, service, node, item_id) - - @defer.inlineCallbacks - def invite(self, client, invitee_jid, service, node, item_id=NS_EVENT): - """Invite an entity to the event - - This will set permission to let the entity access everything needed - @pararm invitee_jid(jid.JID): entity to invite - @param service(jid.JID, None): pubsub service - None to use client's PEP - @param node(unicode): event node - @param item_id(unicode): event id - """ - # FIXME: handle name and extra - name = '' - extra = {} + async def invitePreflight( + self, + client: SatXMPPEntity, + invitee_jid: jid.JID, + service: jid.JID, + node: str, + item_id: Optional[str] = None, + name: str = '', + extra: Optional[dict] = None, + ) -> None: if self._b is None: raise exceptions.FeatureNotFound( _('"XEP-0277" (blog) plugin is needed for this feature') ) if item_id is None: - item_id = NS_EVENT + item_id = extra["default_item_id"] = NS_EVENT - # first we authorize our invitee to see the nodes of interest - yield self._p.setNodeAffiliations(client, service, node, {invitee_jid: "member"}) - log.debug(_("affiliation set on event node")) - __, event_data = yield self.eventGet(client, service, node, item_id) + __, event_data = await self.eventGet(client, service, node, item_id) log.debug(_("got event data")) invitees_service = jid.JID(event_data["invitees_service"]) invitees_node = event_data["invitees_node"] blog_service = jid.JID(event_data["blog_service"]) blog_node = event_data["blog_node"] - yield self._p.setNodeAffiliations( + await self._p.setNodeAffiliations( client, invitees_service, invitees_node, {invitee_jid: "publisher"} ) - log.debug(_("affiliation set on invitee node")) - yield self._p.setNodeAffiliations( + log.debug( + f"affiliation set on invitee node (jid: {invitees_service}, " + f"node: {invitees_node!r})" + ) + await self._p.setNodeAffiliations( client, blog_service, blog_node, {invitee_jid: "member"} ) - blog_items, __ = yield self._b.mbGet(client, blog_service, blog_node, None) + blog_items, __ = await self._b.mbGet(client, blog_service, blog_node, None) for item in blog_items: try: @@ -601,15 +573,15 @@ ) ) else: - yield self._p.setNodeAffiliations( + await self._p.setNodeAffiliations( client, comments_service, comments_node, {invitee_jid: "publisher"} ) log.debug(_("affiliation set on blog and comments nodes")) - # now we send the invitation - pubsub_invitation = self.host.plugins['INVITATION'] - pubsub_invitation.sendPubsubInvitation(client, invitee_jid, service, node, - item_id, name, extra) + def _invite(self, invitee_jid, service, node, item_id, profile): + return self.host.plugins["PUBSUB_INVITATION"]._sendPubsubInvitation( + invitee_jid, service, node, item_id or NS_EVENT, profile_key=profile + ) def _inviteByEmail(self, service, node, id_=NS_EVENT, email="", emails_extra=None, name="", host_name="", language="", url_template="", @@ -631,12 +603,11 @@ ): value = locals()[key] kwargs[key] = str(value) - return self.inviteByEmail( + return defer.ensureDeferred(self.inviteByEmail( client, jid.JID(service) if service else None, node, id_ or NS_EVENT, **kwargs - ) + )) - @defer.inlineCallbacks - def inviteByEmail(self, client, service, node, id_=NS_EVENT, **kwargs): + async def inviteByEmail(self, client, service, node, id_=NS_EVENT, **kwargs): """High level method to create an email invitation to an event @param service(unicode, None): PubSub service @@ -656,11 +627,37 @@ "pubsub", path=service.full(), node=node, item=id_ ) kwargs["extra"] = {"event_uri": event_uri} - invitation_data = yield self._i.create(**kwargs) + invitation_data = await self._i.create(**kwargs) invitee_jid = invitation_data["jid"] log.debug(_("invitation created")) # now that we have a jid, we can send normal invitation - yield self.invite(client, invitee_jid, service, node, id_) + await self.invite(client, invitee_jid, service, node, id_) + + def onInvitationPreflight( + self, + client: SatXMPPEntity, + name: str, + extra: dict, + service: jid.JID, + node: str, + item_id: Optional[str], + item_elt: domish.Element + ) -> None: + event_elt = item_elt.event + link_elt = event_elt.addElement("link") + link_elt["service"] = service.full() + link_elt["node"] = node + link_elt["item"] = item_id + __, event_data = self._parseEventElt(event_elt) + try: + name = event_data["name"] + except KeyError: + pass + else: + extra["name"] = name + if 'image' in event_data: + extra["thumb_url"] = event_data['image'] + extra["element"] = event_elt @implementer(iwokkel.IDisco) diff -r 02a8d227d5bb -r 12dc234f698c sat/plugins/plugin_exp_invitation.py --- a/sat/plugins/plugin_exp_invitation.py Fri Feb 19 15:49:59 2021 +0100 +++ b/sat/plugins/plugin_exp_invitation.py Fri Feb 19 15:50:22 2021 +0100 @@ -16,15 +16,18 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import Optional +from zope.interface import implementer +from twisted.internet import defer +from twisted.words.protocols.jabber import jid +from twisted.words.protocols.jabber.xmlstream import XMPPHandler +from wokkel import disco, iwokkel from sat.core.i18n import _ from sat.core import exceptions from sat.core.constants import Const as C from sat.core.log import getLogger -from twisted.internet import defer -from twisted.words.protocols.jabber import jid -from wokkel import disco, iwokkel -from zope.interface import implementer -from twisted.words.protocols.jabber.xmlstream import XMPPHandler +from sat.core.xmpp import SatXMPPEntity +from sat.tools import utils log = getLogger(__name__) @@ -123,16 +126,25 @@ invitation_elt['thumb_url'] = thumb_url return mess_data, invitation_elt - def sendPubsubInvitation(self, client, invitee_jid, service, node, - item_id, name, extra): + def sendPubsubInvitation( + self, + client: SatXMPPEntity, + invitee_jid: jid.JID, + service: jid.JID, + node: str, + item_id: Optional[str], + name: Optional[str], + extra: Optional[dict] + ) -> None: """Send an pubsub invitation in a stanza - @param invitee_jid(jid.JID): entitee to send invitation to - @param service(jid.JID): pubsub service - @param node(unicode): pubsub node - @param item_id(unicode): pubsub id - @param name(unicode, None): see [_generateBaseInvitation] - @param extra(dict, None): see [_generateBaseInvitation] + @param invitee_jid: entitee to send invitation to + @param service: pubsub service + @param node: pubsub node + @param item_id: pubsub id + None when the invitation is for a whole node + @param name: see [_generateBaseInvitation] + @param extra: see [_generateBaseInvitation] """ if extra is None: extra = {} @@ -141,8 +153,22 @@ pubsub_elt = invitation_elt.addElement("pubsub") pubsub_elt["service"] = service.full() pubsub_elt["node"] = node - pubsub_elt["item"] = item_id - return client.send(mess_data["xml"]) + if item_id is None: + try: + namespace = extra.pop("namespace") + except KeyError: + raise exceptions.DataError('"namespace" key is missing in "extra" data') + node_data_elt = pubsub_elt.addElement("node_data") + node_data_elt["namespace"] = namespace + try: + node_data_elt.addChild(extra["element"]) + except KeyError: + pass + else: + pubsub_elt["item"] = item_id + if "element" in extra: + invitation_elt.addChild(extra.pop("element")) + client.send(mess_data["xml"]) async def sendFileSharingInvitation( self, client, invitee_jid, service, repos_type=None, namespace=None, path=None, @@ -207,53 +233,61 @@ file_sharing_elt["path"] = path client.send(mess_data["xml"]) - @defer.inlineCallbacks - def _parsePubsubElt(self, client, pubsub_elt): + async def _parsePubsubElt(self, client, pubsub_elt): try: service = jid.JID(pubsub_elt["service"]) node = pubsub_elt["node"] - item_id = pubsub_elt.getAttribute("item") except (RuntimeError, KeyError): - log.warning(_("Bad invitation, ignoring")) - raise exceptions.DataError + raise exceptions.DataError("Bad invitation, ignoring") + + item_id = pubsub_elt.getAttribute("item") + + if item_id is not None: + try: + items, metadata = await self._p.getItems( + client, service, node, item_ids=[item_id] + ) + except Exception as e: + log.warning(_("Can't get item linked with invitation: {reason}").format( + reason=e)) + try: + item_elt = items[0] + except IndexError: + log.warning(_("Invitation was linking to a non existing item")) + raise exceptions.DataError - try: - items, metadata = yield self._p.getItems(client, service, node, - item_ids=[item_id]) - except Exception as e: - log.warning(_("Can't get item linked with invitation: {reason}").format( - reason=e)) - try: - item_elt = items[0] - except IndexError: - log.warning(_("Invitation was linking to a non existing item")) - raise exceptions.DataError + try: + namespace = item_elt.firstChildElement().uri + except Exception as e: + log.warning(_("Can't retrieve namespace of invitation: {reason}").format( + reason = e)) + raise exceptions.DataError - try: - namespace = item_elt.firstChildElement().uri - except Exception as e: - log.warning(_("Can't retrieve namespace of invitation: {reason}").format( - reason = e)) - raise exceptions.DataError + args = [service, node, item_id, item_elt] + else: + try: + node_data_elt = next(pubsub_elt.elements((NS_INVITATION, "node_data"))) + except StopIteration: + raise exceptions.DataError("Bad invitation, ignoring") + namespace = node_data_elt['namespace'] + args = [service, node, None, node_data_elt] - args = [service, node, item_id, item_elt] - defer.returnValue((namespace, args)) + return namespace, args - def _parseFileSharingElt(self, client, file_sharing_elt): + async def _parseFileSharingElt(self, client, file_sharing_elt): try: service = jid.JID(file_sharing_elt["service"]) except (RuntimeError, KeyError): log.warning(_("Bad invitation, ignoring")) raise exceptions.DataError repos_type = file_sharing_elt.getAttribute("type", "files") - namespace = file_sharing_elt.getAttribute("namespace") + sharing_ns = file_sharing_elt.getAttribute("namespace") path = file_sharing_elt.getAttribute("path") - args = [service, repos_type, namespace, path] + args = [service, repos_type, sharing_ns, path] ns_fis = self.host.getNamespace("fis") return ns_fis, args - @defer.inlineCallbacks - def onInvitation(self, message_elt, client): + async def onInvitation(self, message_elt, client): log.debug("invitation received [{profile}]".format(profile=client.profile)) invitation_elt = message_elt.invitation @@ -275,7 +309,7 @@ xml = elt.toXml())) continue try: - namespace, args = yield method(client, elt) + namespace, args = await method(client, elt) except exceptions.DataError: log.warning("Can't parse invitation element: {xml}".format( xml = elt.toXml())) @@ -288,7 +322,7 @@ 'No handler for namespace "{namespace}", invitation ignored') .format(namespace=namespace)) else: - cb(client, name, extra, *args) + await utils.asDeferred(cb, client, namespace, name, extra, *args) @implementer(iwokkel.IDisco) @@ -299,7 +333,10 @@ def connectionInitialized(self): self.xmlstream.addObserver( - INVITATION, self.plugin_parent.onInvitation, client=self.parent + INVITATION, + lambda message_elt: defer.ensureDeferred( + self.plugin_parent.onInvitation(message_elt, client=self.parent) + ), ) def getDiscoInfo(self, requestor, target, nodeIdentifier=""): diff -r 02a8d227d5bb -r 12dc234f698c sat/plugins/plugin_exp_invitation_file.py --- a/sat/plugins/plugin_exp_invitation_file.py Fri Feb 19 15:49:59 2021 +0100 +++ b/sat/plugins/plugin_exp_invitation_file.py Fri Feb 19 15:50:22 2021 +0100 @@ -19,6 +19,7 @@ from sat.core.i18n import _ from sat.core.constants import Const as C from sat.core.log import getLogger +from sat.core.xmpp import SatXMPPEntity from sat.tools.common import data_format from twisted.internet import defer from twisted.words.protocols.jabber import jid @@ -69,7 +70,17 @@ extra=extra) ) - def onInvitation(self, client, name, extra, service, repos_type, namespace, path): + def onInvitation( + self, + client: SatXMPPEntity, + namespace: str, + name: str, + extra: dict, + service: jid.JID, + repos_type: str, + sharing_ns: str, + path: str + ): if repos_type == "files": type_human = _("file sharing") elif repos_type == "photos": @@ -81,11 +92,12 @@ type_human = _("file sharing") log.info(_( '{profile} has received an invitation for a files repository ({type_human}) ' - 'with namespace {namespace!r} at path [{path}]').format( - profile=client.profile, type_human=type_human, namespace=namespace, path=path) + 'with namespace {sharing_ns!r} at path [{path}]').format( + profile=client.profile, type_human=type_human, sharing_ns=sharing_ns, + path=path) ) return defer.ensureDeferred( self.host.plugins['LIST_INTEREST'].registerFileSharing( - client, service, repos_type, namespace, path, name, extra + client, service, repos_type, sharing_ns, path, name, extra ) ) diff -r 02a8d227d5bb -r 12dc234f698c sat/plugins/plugin_exp_invitation_pubsub.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat/plugins/plugin_exp_invitation_pubsub.py Fri Feb 19 15:50:22 2021 +0100 @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 + +# SàT plugin to send invitations for Pubsub +# Copyright (C) 2009-2021 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 . + +from typing import Optional +from twisted.internet import defer +from twisted.words.protocols.jabber import jid +from twisted.words.xish import domish +from sat.core.i18n import _ +from sat.core.constants import Const as C +from sat.core.log import getLogger +from sat.core.xmpp import SatXMPPEntity +from sat.tools import utils +from sat.tools.common import data_format + +log = getLogger(__name__) + + +PLUGIN_INFO = { + C.PI_NAME: "Pubsub Invitation", + C.PI_IMPORT_NAME: "PUBSUB_INVITATION", + C.PI_TYPE: "EXP", + C.PI_PROTOCOLS: [], + C.PI_DEPENDENCIES: ["XEP-0060", "INVITATION"], + C.PI_RECOMMENDATIONS: [], + C.PI_MAIN: "PubsubInvitation", + C.PI_HANDLER: "no", + C.PI_DESCRIPTION: _("Invitations for pubsub based features"), +} + + +class PubsubInvitation: + + def __init__(self, host): + log.info(_("Pubsub Invitation plugin initialization")) + self.host = host + self._p = host.plugins["XEP-0060"] + # namespace to handler map + self._ns_handler = {} + host.bridge.addMethod( + "psInvite", + ".plugin", + in_sign="sssssss", + out_sign="", + method=self._sendPubsubInvitation, + async_=True + ) + + def register( + self, + namespace: str, + handler + ) -> None: + self._ns_handler[namespace] = handler + self.host.plugins["INVITATION"].registerNamespace(namespace, self.onInvitation) + + def _sendPubsubInvitation( + self, invitee_jid_s, service_s, node, item_id=None, + name=None, extra_s='', profile_key=C.PROF_KEY_NONE): + client = self.host.getClient(profile_key) + invitee_jid = jid.JID(invitee_jid_s) + service = jid.JID(service_s) + extra = data_format.deserialise(extra_s) + return defer.ensureDeferred( + self.invite( + client, + invitee_jid, + service, + node, + item_id or None, + name=name or None, + extra=extra + ) + ) + + async def invite( + self, + client: SatXMPPEntity, + invitee_jid: jid.JID, + service: jid.JID, + node: str, + item_id: Optional[str] = None, + name: str = '', + extra: Optional[dict] = None, + ) -> None: + if extra is None: + extra = {} + else: + namespace = extra.get("namespace") + if namespace: + try: + handler = self._ns_handler[namespace] + preflight = handler.invitePreflight + except KeyError: + pass + except AttributeError: + log.debug(f"no invitePreflight method found for {namespace!r}") + else: + await utils.asDeferred( + preflight, + client, invitee_jid, service, node, item_id, name, extra + ) + if item_id is None: + item_id = extra.pop("default_item_id", None) + + # we authorize our invitee to see the nodes of interest + await self._p.setNodeAffiliations(client, service, node, {invitee_jid: "member"}) + log.debug(f"affiliation set on {service}'s {node!r} node") + + # now we send the invitation + self.host.plugins["INVITATION"].sendPubsubInvitation( + client, + invitee_jid, + service, + node, + item_id, + name=name or None, + extra=extra + ) + + async def onInvitation( + self, + client: SatXMPPEntity, + namespace: str, + name: str, + extra: dict, + service: jid.JID, + node: str, + item_id: Optional[str], + item_elt: domish.Element + ) -> None: + if extra is None: + extra = {} + try: + handler = self._ns_handler[namespace] + preflight = handler.onInvitationPreflight + except KeyError: + pass + except AttributeError: + log.debug(f"no onInvitationPreflight method found for {namespace!r}") + else: + await utils.asDeferred( + preflight, + client, namespace, name, extra, service, node, item_id, item_elt + ) + if item_id is None: + item_id = extra.pop("default_item_id", None) + creator = extra.pop("creator", False) + element = extra.pop("element", None) + if not name: + name = extra.pop("name", "") + + return self.host.plugins['LIST_INTEREST'].registerPubsub( + client, namespace, service, node, item_id, creator, + name, element, extra)