diff --git a/sat/plugins/plugin_exp_events.py b/sat/plugins/plugin_exp_events.py old mode 100755 new mode 100644 index c9fa5c35..284bf49c --- a/sat/plugins/plugin_exp_events.py +++ b/sat/plugins/plugin_exp_events.py @@ -17,12 +17,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +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 - -log = getLogger(__name__) from sat.tools import utils from sat.tools.common import uri as xmpp_uri from sat.tools.common import date_utils @@ -32,29 +31,24 @@ from twisted.words.xish import domish from wokkel import disco, iwokkel from zope.interface import implements from twisted.words.protocols.jabber.xmlstream import XMPPHandler - from wokkel import pubsub -import shortuuid + +log = getLogger(__name__) PLUGIN_INFO = { - C.PI_NAME: "Event plugin", + C.PI_NAME: "Events", C.PI_IMPORT_NAME: "EVENTS", C.PI_TYPE: "EXP", C.PI_PROTOCOLS: [], - C.PI_DEPENDENCIES: ["XEP-0060"], - C.PI_RECOMMENDATIONS: ["INVITATIONS", "XEP-0277"], + C.PI_DEPENDENCIES: [u"XEP-0060", u"INVITATION", u"LIST_INTEREST"], + C.PI_RECOMMENDATIONS: ["XEP-0277", "EMAIL_INVITATION"], C.PI_MAIN: "Events", C.PI_HANDLER: "yes", - C.PI_DESCRIPTION: _("""Experimental implementation of XMPP events management"""), + C.PI_DESCRIPTION: _(u"""Experimental implementation of XMPP events management"""), } NS_EVENT = "org.salut-a-toi.event:0" -NS_EVENT_LIST = NS_EVENT + "#list" -NS_EVENT_INVIT = NS_EVENT + "#invitation" -INVITATION = '/message[@type="chat"]/invitation[@xmlns="{ns_invit}"]'.format( - ns_invit=NS_EVENT_INVIT -) class Events(object): @@ -64,8 +58,10 @@ class Events(object): log.info(_(u"Event plugin initialization")) self.host = host self._p = self.host.plugins["XEP-0060"] - self._i = self.host.plugins.get("INVITATIONS") + self._i = self.host.plugins.get("EMAIL_INVITATION") self._b = self.host.plugins.get("XEP-0277") + self.host.plugins[u"INVITATION"].registerNamespace(NS_EVENT, + self.register) host.bridge.addMethod( "eventGet", ".plugin", @@ -229,11 +225,12 @@ class Events(object): items, metadata = yield self._p.getItems(client, service, node, item_ids=[id_]) try: event_elt = next(items[0].elements(NS_EVENT, u"event")) + except StopIteration: + raise exceptions.NotFound(_(u"No event element has been found")) except IndexError: raise exceptions.NotFound(_(u"No event with this id has been found")) defer.returnValue(event_elt) - @defer.inlineCallbacks def register(self, client, service, node, event_id, event_elt, creator=False): """register evenement in personal events list @@ -244,32 +241,13 @@ class Events(object): note that this element will be modified in place @param creator(bool): True if client's profile is the creator of the node """ - # we save a link to the event in our local list - try: - # TODO: check auto-create, no need to create node first if available - options = {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_WHITELIST} - yield self._p.createNode( - client, - client.jid.userhostJID(), - nodeIdentifier=NS_EVENT_LIST, - options=options, - ) - except error.StanzaError as e: - if e.condition == u"conflict": - log.debug(_(u"requested node already exists")) - link_elt = event_elt.addElement((NS_EVENT_LIST, "link")) + link_elt = event_elt.addElement("link") link_elt["service"] = service.full() link_elt["node"] = node link_elt["item"] = event_id - item_id = xmpp_uri.buildXMPPUri( - u"pubsub", path=service.full(), node=node, item=event_id - ) - if creator: - event_elt["creator"] = "true" - item_elt = pubsub.Item(id=item_id, payload=event_elt) - yield self._p.publish( - client, client.jid.userhostJID(), NS_EVENT_LIST, items=[item_elt] - ) + return self.host.plugins[u'LIST_INTEREST'].registerPubsub( + client, NS_EVENT, service, node, event_id, creator, + element=event_elt) def _eventGet(self, service, node, id_=u"", profile_key=C.PROF_KEY_NONE): service = jid.JID(service) if service else None @@ -396,17 +374,11 @@ class Events(object): yield self.register(client, service, node, event_id, event_elt, creator=True) defer.returnValue(node) - def _eventModify( - self, - service, - node, - id_, - timestamp_update, - data_update, - profile_key=C.PROF_KEY_NONE, - ): + def _eventModify(self, service, node, id_, timestamp_update, data_update, + profile_key=C.PROF_KEY_NONE): service = jid.JID(service) if service else None - node = node if node else NS_EVENT + if not node: + raise ValueError(_(u"missing node")) client = self.host.getClient(profile_key) return self.eventModify( client, service, node, id_ or NS_EVENT, timestamp_update or None, data_update @@ -444,24 +416,24 @@ class Events(object): return d @defer.inlineCallbacks - def eventsList(self, client, service, node): + def eventsList(self, client, service, node=None): """Retrieve list of registered events @return list(tuple(int, dict)): list of events (timestamp + metadata) """ - if not node: - node = NS_EVENT_LIST - items = yield self._p.getItems(client, service, node) + items, metadata = yield self.host.plugins[u'LIST_INTEREST'].listInterests( + client, service, node, namespace=NS_EVENT) events = [] - for item in items[0]: + for item in items: try: - event_elt = next(item.elements(NS_EVENT, u"event")) + event_elt = next(item.interest.pubsub.elements(NS_EVENT, u"event")) except IndexError: - log.error( + log.warning( _(u"No event found in item {item_id}").format(item_id=item["id"]) ) - timestamp, data = self._parseEventElt(event_elt) - events.append((timestamp, data)) + else: + timestamp, data = self._parseEventElt(event_elt) + events.append((timestamp, data)) defer.returnValue(events) def _eventInviteeGet(self, service, node, profile_key): @@ -479,12 +451,12 @@ class Events(object): @return (dict): a dict with current attendance status, an empty dict is returned if nothing has been answered yed """ - items, metadata = yield self._p.getItems( - client, service, node, item_ids=[client.jid.userhost()] - ) try: + items, metadata = yield self._p.getItems( + client, service, node, item_ids=[client.jid.userhost()] + ) event_elt = next(items[0].elements(NS_EVENT, u"invitee")) - except IndexError: + except (exceptions.NotFound, IndexError): # no item found, event data are not set yet defer.returnValue({}) data = {} @@ -542,13 +514,9 @@ class Events(object): event_elt = next(item.elements(NS_EVENT, u"invitee")) except StopIteration: # no item found, event data are not set yet - log.warning( - _( - u"no data found for {item_id} (service: {service}, node: {node})".format( - item_id=item["id"], service=service, node=node - ) - ) - ) + log.warning(_( + u"no data found for {item_id} (service: {service}, node: {node})" + .format(item_id=item["id"], service=service, node=node))) else: data = {} for key in (u"attend", u"guests"): @@ -559,30 +527,6 @@ class Events(object): invitees[item["id"]] = data defer.returnValue(invitees) - def sendMessageInvitation(self, client, invitee_jid, service, node, item_id): - """Send an invitation in a stanza - - @param invitee_jid(jid.JID): entitee to send invitation to - @param service(jid.JID): pubsub service of the event - @param node(unicode): node of the event - @param item_id(unicode): id of the event - """ - mess_data = { - "from": client.jid, - "to": invitee_jid, - "uid": "", - "message": {}, - "type": C.MESS_TYPE_CHAT, - "subject": {}, - "extra": {}, - } - client.generateMessageXML(mess_data) - event_elt = mess_data["xml"].addElement("invitation", NS_EVENT_INVIT) - event_elt["service"] = service.full() - event_elt["node"] = node - event_elt["item"] = item_id - client.send(mess_data["xml"]) - def _invite(self, invitee_jid, service, node, item_id, profile): client = self.host.getClient(profile) service = jid.JID(service) if service else None @@ -644,23 +588,14 @@ class Events(object): log.debug(_(u"affiliation set on blog and comments nodes")) # now we send the invitation - self.sendMessageInvitation(client, invitee_jid, service, node, item_id) + pubsub_invitation = self.host.plugins[u'PUBSUB_INVITATION'] + pubsub_invitation.sendPubsubInvitation(client, invitee_jid, service, node, + item_id) - def _inviteByEmail( - self, - service, - node, - id_=NS_EVENT, - email=u"", - emails_extra=None, - name=u"", - host_name=u"", - language=u"", - url_template=u"", - message_subject=u"", - message_body=u"", - profile_key=C.PROF_KEY_NONE, - ): + def _inviteByEmail(self, service, node, id_=NS_EVENT, email=u"", emails_extra=None, + name=u"", host_name=u"", language=u"", url_template=u"", + message_subject=u"", message_body=u"", + profile_key=C.PROF_KEY_NONE): client = self.host.getClient(profile_key) kwargs = { u"profile": client.profile, @@ -708,19 +643,6 @@ class Events(object): # now that we have a jid, we can send normal invitation yield self.invite(client, invitee_jid, service, node, id_) - @defer.inlineCallbacks - def onInvitation(self, message_elt, client): - invitation_elt = message_elt.invitation - try: - service = jid.JID(invitation_elt["service"]) - node = invitation_elt["node"] - event_id = invitation_elt["item"] - except (RuntimeError, KeyError): - log.warning(_(u"Bad invitation: {xml}").format(xml=message_elt.toXml())) - - event_elt = yield self.getEventElement(client, service, node, event_id) - yield self.register(client, service, node, event_id, event_elt, creator=False) - class EventsHandler(XMPPHandler): implements(iwokkel.IDisco) @@ -728,20 +650,9 @@ class EventsHandler(XMPPHandler): def __init__(self, plugin_parent): self.plugin_parent = plugin_parent - @property - def host(self): - return self.plugin_parent.host - - def connectionInitialized(self): - self.xmlstream.addObserver( - INVITATION, self.plugin_parent.onInvitation, client=self.parent - ) - def getDiscoInfo(self, requestor, target, nodeIdentifier=""): return [ disco.DiscoFeature(NS_EVENT), - disco.DiscoFeature(NS_EVENT_LIST), - disco.DiscoFeature(NS_EVENT_INVIT), ] def getDiscoItems(self, requestor, target, nodeIdentifier=""): diff --git a/sat/plugins/plugin_exp_invitation.py b/sat/plugins/plugin_exp_invitation.py new file mode 100644 index 00000000..94b5a040 --- /dev/null +++ b/sat/plugins/plugin_exp_invitation.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# SAT plugin to detect language (experimental) +# Copyright (C) 2009-2019 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 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 implements +from twisted.words.protocols.jabber.xmlstream import XMPPHandler + +log = getLogger(__name__) + + +PLUGIN_INFO = { + C.PI_NAME: "Invitation", + C.PI_IMPORT_NAME: "INVITATION", + C.PI_TYPE: "EXP", + C.PI_PROTOCOLS: [], + C.PI_DEPENDENCIES: ["XEP-0060"], + C.PI_RECOMMENDATIONS: [], + C.PI_MAIN: "Invitation", + C.PI_HANDLER: "yes", + C.PI_DESCRIPTION: _(u"Experimental handling of invitations"), +} + +NS_INVITATION = u"https://salut-a-toi/protocol/invitation:0" +INVITATION = '/message/invitation[@xmlns="{ns_invit}"]'.format( + ns_invit=NS_INVITATION +) +NS_INVITATION_LIST = NS_INVITATION + u"#list" + + +class Invitation(object): + + def __init__(self, host): + log.info(_(u"Invitation plugin initialization")) + self.host = host + self._p = self.host.plugins["XEP-0060"] + # map from namespace of the invitation to callback handling it + self._ns_cb = {} + + def getHandler(self, client): + return PubsubInvitationHandler(self) + + def registerNamespace(self, namespace, callback): + """Register a callback for a namespace + + @param namespace(unicode): namespace handled + @param callback(callbable): method handling the invitation + For pubsub invitation, it will be called with following arguments: + - client + - service(jid.JID): pubsub service jid + - node(unicode): pubsub node + - item_id(unicode, None): pubsub item id + - item_elt(domish.Element): item of the invitation + @raise exceptions.ConflictError: this namespace is already registered + """ + if namespace in self._ns_cb: + raise exceptions.ConflictError( + u"invitation namespace {namespace} is already register with {callback}" + .format(namespace=namespace, callback=self._ns_cb[namespace])) + self._ns_cb[namespace] = callback + + def sendPubsubInvitation(self, client, invitee_jid, service, node, + item_id): + """Send an 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 + """ + mess_data = { + "from": client.jid, + "to": invitee_jid, + "uid": "", + "message": {}, + "type": C.MESS_TYPE_CHAT, + "subject": {}, + "extra": {}, + } + client.generateMessageXML(mess_data) + invitation_elt = mess_data["xml"].addElement("invitation", NS_INVITATION) + pubsub_elt = invitation_elt.addElement(u"pubsub") + pubsub_elt[u"service"] = service.full() + pubsub_elt[u"node"] = node + pubsub_elt[u"item"] = item_id + client.send(mess_data[u"xml"]) + + @defer.inlineCallbacks + 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(_(u"Bad invitation, ignoring")) + raise exceptions.DataError + + try: + items, metadata = yield self._p.getItems(client, service, node, + item_ids=[item_id]) + except Exception as e: + log.warning(_(u"Can't get item linked with invitation: {reason}").format( + reason=e)) + try: + item_elt = items[0] + except IndexError: + log.warning(_(u"Invitation was linking to a non existing item")) + raise exceptions.DataError + + try: + namespace = item_elt.firstChildElement().uri + except Exception as e: + log.warning(_(u"Can't retrieve namespace of invitation: {reason}").format( + reason = e)) + raise exceptions.DataError + + args = [service, node, item_id, item_elt] + defer.returnValue((namespace, args)) + + @defer.inlineCallbacks + def onInvitation(self, message_elt, client): + invitation_elt = message_elt.invitation + for elt in invitation_elt.elements(): + if elt.uri != NS_INVITATION: + log.warning(u"unexpected element: {xml}".format(xml=elt.toXml())) + continue + if elt.name == u"pubsub": + method = self._parsePubsubElt + else: + log.warning(u"not implemented invitation element: {xml}".format( + xml = elt.toXml())) + continue + try: + namespace, args = yield method(client, elt) + except exceptions.DataError: + log.warning(u"Can't parse invitation element: {xml}".format( + xml = elt.toXml())) + continue + + try: + cb = self._ns_cb[namespace] + except KeyError: + log.warning(_(u'No handler for namespace "{namespace}", invitation ignored') + .format(namespace=namespace)) + else: + cb(client, *args) + + +class PubsubInvitationHandler(XMPPHandler): + implements(iwokkel.IDisco) + + def __init__(self, plugin_parent): + self.plugin_parent = plugin_parent + + def connectionInitialized(self): + self.xmlstream.addObserver( + INVITATION, self.plugin_parent.onInvitation, client=self.parent + ) + + def getDiscoInfo(self, requestor, target, nodeIdentifier=""): + return [ + disco.DiscoFeature(NS_INVITATION), + ] + + def getDiscoItems(self, requestor, target, nodeIdentifier=""): + return [] diff --git a/sat/plugins/plugin_exp_list_of_interest.py b/sat/plugins/plugin_exp_list_of_interest.py new file mode 100644 index 00000000..07987211 --- /dev/null +++ b/sat/plugins/plugin_exp_list_of_interest.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# SAT plugin to detect language (experimental) +# Copyright (C) 2009-2019 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 sat.core.i18n import _ +from sat.core.constants import Const as C +from sat.core.log import getLogger +from wokkel import disco, iwokkel, pubsub +from zope.interface import implements +from twisted.internet import defer +from twisted.words.protocols.jabber import error as jabber_error +from twisted.words.protocols.jabber.xmlstream import XMPPHandler +from twisted.words.xish import domish + +log = getLogger(__name__) + + +PLUGIN_INFO = { + C.PI_NAME: "List of Interest", + C.PI_IMPORT_NAME: "LIST_INTEREST", + C.PI_TYPE: "EXP", + C.PI_PROTOCOLS: [], + C.PI_DEPENDENCIES: ["XEP-0060"], + C.PI_RECOMMENDATIONS: [], + C.PI_MAIN: "ListInterest", + C.PI_HANDLER: "yes", + C.PI_DESCRIPTION: _(u"Experimental handling of interesting XMPP locations"), +} + +NS_LIST_INTEREST = "https://salut-a-toi/protocol/list-interest:0" + + +class ListInterest(object): + namespace = NS_LIST_INTEREST + + def __init__(self, host): + log.info(_(u"List of Interest plugin initialization")) + self.host = host + self._p = self.host.plugins["XEP-0060"] + + def getHandler(self, client): + return ListInterestHandler(self) + + @defer.inlineCallbacks + def createNode(self, client): + try: + # TODO: check auto-create, no need to create node first if available + options = {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_WHITELIST} + yield self._p.createNode( + client, + client.jid.userhostJID(), + nodeIdentifier=NS_LIST_INTEREST, + options=options, + ) + except jabber_error.StanzaError as e: + if e.condition == u"conflict": + log.debug(_(u"requested node already exists")) + + @defer.inlineCallbacks + def registerPubsub(self, client, namespace, service, node, item_id=None, + creator=False, name=None, element=None): + """Register an interesting element in personal list + + @param namespace(unicode): namespace of the interest + this is used as a cache, to avoid the need to retrieve the item only to get + its namespace + @param service(jid.JID): pubsub service of the + @param node(unicode): target pubsub node + @param item_id(unicode, None): target pubsub id + @param creator(bool): True if client's profile is the creator of the node + This is used a cache, to avoid the need to retrieve affiliations + @param name(unicode, None): name of the interest + @param element(domish.Element, None): element to attach + may be used to cache some extra data + """ + yield self.createNode(client) + interest_elt = domish.Element((NS_LIST_INTEREST, u"interest")) + interest_elt[u"namespace"] = namespace + if name is not None: + interest_elt[u'name'] = name + pubsub_elt = interest_elt.addElement(u"pubsub") + pubsub_elt[u"service"] = service.full() + pubsub_elt[u"node"] = node + if item_id is not None: + pubsub_elt[u"item"] = item_id + if creator: + pubsub_elt[u"creator"] = C.BOOL_TRUE + if element is not None: + pubsub_elt.addChild(element) + item_elt = pubsub.Item(payload=interest_elt) + yield self._p.publish( + client, client.jid.userhostJID(), NS_LIST_INTEREST, items=[item_elt] + ) + + @defer.inlineCallbacks + def listInterests(self, client, service=None, node=None, namespace=None): + """Retrieve list of interests + + @param service(jid.JID, None): service to use + None to use own PEP + @param node(unicode, None): node to use + None to use default node + @param namespace(unicode, None): filter interests of this namespace + None to retrieve all interests + @return: same as [XEP_0060.getItems] + """ + # TODO: if a MAM filter were available, it would improve performances + if not node: + node = NS_LIST_INTEREST + items, metadata = yield self._p.getItems(client, service, node) + if namespace is not None: + filtered_items = [] + for item in items: + try: + interest_elt = next(item.elements(NS_LIST_INTEREST, u"interest")) + except StopIteration: + log.warning(_(u"Missing interest element: {xml}").format( + xml=interest_elt.toXml())) + continue + if interest_elt.getAttribute(u"namespace") == namespace: + filtered_items.append(item) + items = filtered_items + + defer.returnValue((items, metadata)) + + +class ListInterestHandler(XMPPHandler): + implements(iwokkel.IDisco) + + def __init__(self, plugin_parent): + self.plugin_parent = plugin_parent + + def getDiscoInfo(self, requestor, target, nodeIdentifier=""): + return [ + disco.DiscoFeature(NS_LIST_INTEREST), + ] + + def getDiscoItems(self, requestor, target, nodeIdentifier=""): + return [] diff --git a/sat/plugins/plugin_misc_invitations.py b/sat/plugins/plugin_misc_email_invitation.py similarity index 99% rename from sat/plugins/plugin_misc_invitations.py rename to sat/plugins/plugin_misc_email_invitation.py index 89f94675..9349e095 100644 --- a/sat/plugins/plugin_misc_invitations.py +++ b/sat/plugins/plugin_misc_email_invitation.py @@ -35,7 +35,7 @@ log = getLogger(__name__) PLUGIN_INFO = { C.PI_NAME: "Invitations", - C.PI_IMPORT_NAME: "INVITATIONS", + C.PI_IMPORT_NAME: "EMAIL_INVITATION", C.PI_TYPE: C.PLUG_TYPE_MISC, C.PI_DEPENDENCIES: ['XEP-0077'], C.PI_RECOMMENDATIONS: ["IDENTITY"],