changeset 2243:5e12fc5ae52a

plugin events: separation of event node and invitees node - event node is handling the main metadata of the event, and invitees node handle the invitations/invitees answers - invitees and blog node are automatically created and associated to the event, except if they are specified (in which cas the existing one are used and attached to the event node) - extra metadata are added to <meta> elements
author Goffi <goffi@goffi.org>
date Fri, 19 May 2017 12:43:41 +0200
parents e5e54ff0b775
children 9d49e66bdbf2
files src/plugins/plugin_exp_events.py src/plugins/plugin_xep_0277.py
diffstat 2 files changed, 212 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- a/src/plugins/plugin_exp_events.py	Fri May 19 12:40:03 2017 +0200
+++ b/src/plugins/plugin_exp_events.py	Fri May 19 12:43:41 2017 +0200
@@ -18,11 +18,14 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 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 uri_parse
 from twisted.internet import defer
-from twisted.words.protocols.jabber import jid
+from twisted.words.protocols.jabber import jid, error
 from twisted.words.xish import domish
 from wokkel import pubsub
 
@@ -49,22 +52,218 @@
         self.host = host
         self._p = self.host.plugins["XEP-0060"]
         host.bridge.addMethod("eventGet", ".plugin",
-                              in_sign='sss', out_sign='a{ss}',
+                              in_sign='ssss', out_sign='(ia{ss})',
                               method=self._eventGet,
                               async=True)
-        host.bridge.addMethod("eventSet", ".plugin",
+        host.bridge.addMethod("eventCreate", ".plugin",
+                              in_sign='ia{ss}ssss', out_sign='s',
+                              method=self._eventCreate,
+                              async=True)
+        host.bridge.addMethod("eventModify", ".plugin",
+                              in_sign='sssia{ss}s', out_sign='',
+                              method=self._eventModify,
+                              async=True)
+        host.bridge.addMethod("eventInviteeGet", ".plugin",
+                              in_sign='sss', out_sign='a{ss}',
+                              method=self._eventInviteeGet,
+                              async=True)
+        host.bridge.addMethod("eventInviteeSet", ".plugin",
                               in_sign='ssa{ss}s', out_sign='',
-                              method=self._eventSet,
+                              method=self._eventInviteeSet,
                               async=True)
 
-    def _eventGet(self, service, node, profile_key):
+    def _eventGet(self, service, node, id_=u'', profile_key=C.PROF_KEY_NONE):
         service = jid.JID(service) if service else None
         node = node if node else NS_EVENT
         client = self.host.getClient(profile_key)
-        return self.eventGet(client, service, node)
+        return self.eventGet(client, service, node, id_)
+
+    @defer.inlineCallbacks
+    def eventGet(self, client, service, node, id_=NS_EVENT):
+        """Retrieve event data
+
+        @param service(unicode, None): PubSub service
+        @param node(unicode): PubSub node of the event
+        @param id_(unicode): id_ with even data
+        @return (tuple[int, dict[unicode, unicode]): event data:
+            - timestamp of the event
+            - event metadata where key can be:
+                location: location of the event
+                image: URL of a picture to use to represent event
+                background-image: URL of a picture to use in background
+            an empty dict is returned if nothing has been answered yed
+        """
+        if not id_:
+            id_ = NS_EVENT
+        items, metadata = yield self._p.getItems(service, node, item_ids=[id_], profile_key=client.profile)
+        try:
+            event_elt = next(items[0].elements(NS_EVENT, u'event'))
+        except IndexError:
+            raise exceptions.NotFound(_(u"No event with this id has been found"))
+
+        try:
+            timestamp = utils.date_parse(next(event_elt.elements(NS_EVENT, "date")))
+        except StopIteration:
+            timestamp = -1
+
+        data = {}
+
+        for key in (u'name',):
+            try:
+                data[key] = event_elt[key]
+            except KeyError:
+                continue
+
+        for elt_name in (u'description',):
+            try:
+                elt = next(event_elt.elements(NS_EVENT, elt_name))
+            except StopIteration:
+                continue
+            else:
+                data[elt_name] = unicode(elt)
+
+        for elt_name in (u'image', 'background-image'):
+            try:
+                image_elt = next(event_elt.elements(NS_EVENT, elt_name))
+                data[elt_name] = image_elt['src']
+            except StopIteration:
+                continue
+            except KeyError:
+                log.warning(_(u'no src found for image'))
+
+        for uri_type in (u'invitees', u'blog'):
+            try:
+                elt = next(event_elt.elements(NS_EVENT, 'invitees'))
+                uri = data[uri_type + u'_uri'] = elt['uri']
+                uri_data = uri_parse.parseXMPPUri(uri)
+                if uri_data[u'type'] != u'pubsub':
+                    raise ValueError
+            except StopIteration:
+                log.warning(_(u"no {uri_type} element found!").format(uri_type=uri_type))
+            except KeyError:
+                log.warning(_(u"incomplete {uri_type} element").format(uri_type=uri_type))
+            except ValueError:
+                log.warning(_(u"bad {uri_type} element").format(uri_type=uri_type))
+            else:
+                data[uri_type + u'_service'] = uri_data[u'path']
+                data[uri_type + u'_node'] = uri_data[u'node']
+
+        for meta_elt in event_elt.elements(NS_EVENT, 'meta'):
+            key = meta_elt[u'name']
+            if key in data:
+                log.warning(u'Ignoring conflicting meta element: {xml}'.format(xml=meta_elt.toXml()))
+                continue
+            data[key] = unicode(meta_elt)
+
+        defer.returnValue((timestamp, data))
+
+    def _eventCreate(self, timestamp, data, service, node, id_=u'', profile_key=C.PROF_KEY_NONE):
+        service = jid.JID(service) if service else None
+        node = node if node else NS_EVENT
+        client = self.host.getClient(profile_key)
+        return self.eventCreate(client, timestamp, data, service, node, id_ or NS_EVENT)
 
     @defer.inlineCallbacks
-    def eventGet(self, client, service, node):
+    def eventCreate(self, client, timestamp, data, service, node=None, item_id=NS_EVENT):
+        """Create or replace an event
+
+        @param service(jid.JID, None): PubSub service
+        @param node(unicode, None): PubSub node of the event
+            None will create instant node.
+        @param item_id(unicode): ID of the item to create.
+        @param timestamp(timestamp, None)
+        @param data(dict[unicode, unicode]): data to update
+            dict will be cleared, do a copy if data are still needed
+            key can be:
+                - name: name of the event
+                - description: details
+                - image: main picture of the event
+                - background-image: image to use as background
+        @return (unicode): created node
+        """
+        if not item_id:
+            raise ValueError(_(u"item_id must be set"))
+        if not service:
+            service = client.jid.userhostJID()
+        event_elt = domish.Element((NS_EVENT, 'event'))
+        if timestamp is not None and timestamp != -1:
+            formatted_date = utils.xmpp_date(timestamp)
+            event_elt.addElement((NS_EVENT, 'date'), content=formatted_date)
+        for key in (u'name',):
+            if key in data:
+                event_elt[key] = data.pop(key)
+        for key in (u'description',):
+            if key in data:
+                event_elt.addElement((NS_EVENT, key), content=data.pop(key))
+        for key in (u'image', u'background-image'):
+            if key in data:
+                elt = event_elt.addElement((NS_EVENT, key))
+                elt['src'] = data.pop(key)
+
+        # we first create the invitees and blog nodes (if not specified in data)
+        for uri_type in (u'invitees', u'blog'):
+            key = uri_type + u'_uri'
+            if key not in data:
+                # FIXME: affiliate invitees
+                uri_node = yield self._p.createNode(client, service)
+                yield self._p.setConfiguration(client, service, uri_node, {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_WHITELIST})
+                uri_service = service
+            else:
+                # we suppose that *_service and *_node are present
+                # FIXME: handle cases when they are not
+                uri_service = data.pop(uri_type + u'_service')
+                uri_node = data.pop(uri_type + u'_node')
+                del data[key]
+
+            elt = event_elt.addElement((NS_EVENT, uri_type))
+            elt['uri'] = uri_parse.buildXMPPUri('pubsub', path=uri_service.full(), node=uri_node)
+
+        # remaining data are put in <meta> elements
+        for key in data.keys():
+            elt = event_elt.addElement((NS_EVENT, 'meta'), content = data.pop(key))
+            elt['name'] = key
+
+        item_elt = pubsub.Item(id=item_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)
+        except error.StanzaError as e:
+            if e.condition == u'conflict':
+                log.debug(_(u"requested node already exists"))
+
+        yield self._p.publish(service, node, items=[item_elt], profile_key=client.profile)
+
+        defer.returnValue(node)
+
+    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
+        client = self.host.getClient(profile_key)
+        return self.eventModify(client, service, node, id_ or NS_EVENT, timestamp_update or None, data_update)
+
+    @defer.inlineCallbacks
+    def eventModify(self, client, service, node, id_=NS_EVENT, timestamp_update=None, data_update=None):
+        """Update an event
+
+        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_)
+        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.iteritems():
+                new_data[k] = v
+        yield self.eventCreate(client, new_timestamp, new_data, service, node, id_)
+
+    def _eventInviteeGet(self, service, node, profile_key):
+        service = jid.JID(service) if service else None
+        node = node if node else NS_EVENT
+        client = self.host.getClient(profile_key)
+        return self.eventInviteeGet(client, service, node)
+
+    @defer.inlineCallbacks
+    def eventInviteeGet(self, client, service, node):
         """Retrieve attendance from event node
 
         @param service(unicode, None): PubSub service
@@ -74,7 +273,7 @@
         """
         items, metadata = yield self._p.getItems(service, node, item_ids=[client.jid.userhost()], profile_key=client.profile)
         try:
-            event_elt = next(items[0].elements((NS_EVENT, u'event')))
+            event_elt = next(items[0].elements(NS_EVENT, u'invitee'))
         except IndexError:
             # no item found, event data are not set yet
             defer.returnValue({})
@@ -86,13 +285,13 @@
                 continue
         defer.returnValue(data)
 
-    def _eventSet(self, service, node, event_data,  profile_key):
+    def _eventInviteeSet(self, service, node, event_data,  profile_key):
         service = jid.JID(service) if service else None
         node = node if node else NS_EVENT
         client = self.host.getClient(profile_key)
-        return self.eventSet(client, service, node, event_data)
+        return self.eventInviteeSet(client, service, node, event_data)
 
-    def eventSet(self, client, service, node, data):
+    def eventInviteeSet(self, client, service, node, data):
         """Set or update attendance data in event node
 
         @param service(unicode, None): PubSub service
@@ -102,7 +301,7 @@
                 attend: one of "yes", "no", "maybe"
                 guests: an int
         """
-        event_elt = domish.Element((NS_EVENT, 'event'))
+        event_elt = domish.Element((NS_EVENT, 'invitee'))
         for key in (u'attend', u'guests'):
             try:
                 event_elt[key] = data.pop(key)
--- a/src/plugins/plugin_xep_0277.py	Fri May 19 12:40:03 2017 +0200
+++ b/src/plugins/plugin_xep_0277.py	Fri May 19 12:43:41 2017 +0200
@@ -31,7 +31,7 @@
 from sat.tools import utils
 from sat.tools.common import data_format
 
-# XXX: tmp.pubsub is actually use instead of wokkel version
+# XXX: tmp.pubsub is actually used instead of wokkel version
 from wokkel import pubsub
 import uuid
 import time