diff --git a/sat/plugins/plugin_exp_events.py b/sat/plugins/plugin_exp_events.py
index f5fed781..7b2cb08f 100755
--- a/sat/plugins/plugin_exp_events.py
+++ b/sat/plugins/plugin_exp_events.py
@@ -23,12 +23,17 @@ 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 sat.tools.common import uri as xmpp_uri
from sat.tools.common import date_utils
from twisted.internet import defer
from twisted.words.protocols.jabber import jid, error
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
PLUGIN_INFO = {
@@ -39,11 +44,15 @@ PLUGIN_INFO = {
C.PI_DEPENDENCIES: ["XEP-0060"],
C.PI_RECOMMENDATIONS: ["INVITATIONS", "XEP-0277"],
C.PI_MAIN: "Events",
- C.PI_HANDLER: "no",
+ C.PI_HANDLER: "yes",
C.PI_DESCRIPTION: _("""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):
@@ -67,6 +76,10 @@ class Events(object):
in_sign='sssia{ss}s', out_sign='',
method=self._eventModify,
async=True)
+ host.bridge.addMethod("eventsList", ".plugin",
+ in_sign='sss', out_sign='aa{ss}',
+ method=self._eventsList,
+ async=True)
host.bridge.addMethod("eventInviteeGet", ".plugin",
in_sign='sss', out_sign='a{ss}',
method=self._eventInviteeGet,
@@ -79,38 +92,22 @@ class Events(object):
in_sign='sss', out_sign='a{sa{ss}}',
method=self._eventInviteesList,
async=True),
- host.bridge.addMethod("eventInvite", ".plugin", in_sign='ssssassssssss', out_sign='',
+ host.bridge.addMethod("eventInvite", ".plugin", in_sign='sssss', out_sign='',
method=self._invite,
async=True)
+ host.bridge.addMethod("eventInviteByEmail", ".plugin", in_sign='ssssassssssss', out_sign='',
+ method=self._inviteByEmail,
+ async=True)
- 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, id_)
+ def getHandler(self, client):
+ return EventsHandler(self)
- @defer.inlineCallbacks
- def eventGet(self, client, service, node, id_=NS_EVENT):
- """Retrieve event data
+ def _parseEventElt(self, event_elt):
+ """Helper method to parse event element
- @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
+ @param (domish.Element): event_elt
+ @return (tupple[int, dict[unicode, unicode]): timestamp, event_data
"""
- if not id_:
- id_ = NS_EVENT
- items, metadata = yield self._p.getItems(client, service, node, item_ids=[id_])
- 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 = date_utils.date_parse(next(event_elt.elements(NS_EVENT, "date")))
except StopIteration:
@@ -145,7 +142,7 @@ class Events(object):
try:
elt = next(event_elt.elements(NS_EVENT, uri_type))
uri = data[uri_type + u'_uri'] = elt['uri']
- uri_data = uri_parse.parseXMPPUri(uri)
+ uri_data = xmpp_uri.parseXMPPUri(uri)
if uri_data[u'type'] != u'pubsub':
raise ValueError
except StopIteration:
@@ -164,23 +161,111 @@ class Events(object):
log.warning(u'Ignoring conflicting meta element: {xml}'.format(xml=meta_elt.toXml()))
continue
data[key] = unicode(meta_elt)
+ if event_elt.link:
+ link_elt = event_elt.link
+ data['service'] = link_elt['service']
+ data['node'] = link_elt['node']
+ data['item'] = link_elt['item']
+ if event_elt.getAttribute('creator') == 'true':
+ data['creator'] = True
+ return timestamp, data
- defer.returnValue((timestamp, data))
+ @defer.inlineCallbacks
+ def getEventElement(self, client, service, node, id_):
+ """Retrieve event element
- def _eventCreate(self, timestamp, data, service, node, id_=u'', profile_key=C.PROF_KEY_NONE):
+ @param service(jid.JID): pubsub service
+ @param node(unicode): pubsub node
+ @param id_(unicode, None): event id
+ @return (domish.Element): event element
+ @raise exceptions.NotFound: no event element found
+ """
+ if not id_:
+ id_ = NS_EVENT
+ items, metadata = yield self._p.getItems(client, service, node, item_ids=[id_])
+ 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"))
+ defer.returnValue(event_elt)
+
+ @defer.inlineCallbacks
+ def register(self, client, service, node, event_id, event_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
+ """
+ # 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["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])
+
+ 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, 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
+ """
+ event_elt = yield self.getEventElement(client, service, node, id_)
+
+ defer.returnValue(self._parseEventElt(event_elt))
+
+ 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 or None
+ client = self.host.getClient(profile_key)
+ data[u'register'] = C.bool(data.get(u'register', C.BOOL_FALSE))
return self.eventCreate(client, timestamp, data, service, node, id_ or NS_EVENT)
@defer.inlineCallbacks
- def eventCreate(self, client, timestamp, data, service, node=None, item_id=NS_EVENT):
+ 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
@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 event_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
@@ -189,16 +274,20 @@ class Events(object):
- description: details
- image: main picture of the event
- background-image: image to use as background
+ - register: bool, True if we want to register the event in our local list
@return (unicode): created node
"""
- if not item_id:
- raise ValueError(_(u"item_id must be set"))
+ if not event_id:
+ raise ValueError(_(u"event_id must be set"))
if not service:
service = client.jid.userhostJID()
+ if not node:
+ node = NS_EVENT + u'__' + shortuuid.uuid()
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)
+ register = data.pop('register', False)
for key in (u'name',):
if key in data:
event_elt[key] = data.pop(key)
@@ -224,21 +313,21 @@ class Events(object):
uri_service = service
else:
uri = data.pop(key)
- uri_data = uri_parse.parseXMPPUri(uri)
+ uri_data = xmpp_uri.parseXMPPUri(uri)
if uri_data[u'type'] != u'pubsub':
raise ValueError(_(u'The given URI is not valid: {uri}').format(uri=uri))
uri_service = jid.JID(uri_data[u'path'])
uri_node = uri_data[u'node']
elt = event_elt.addElement((NS_EVENT, uri_type))
- elt['uri'] = uri_parse.buildXMPPUri('pubsub', path=uri_service.full(), node=uri_node)
+ elt['uri'] = xmpp_uri.buildXMPPUri('pubsub', path=uri_service.full(), node=uri_node)
# remaining data are put in 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)
+ 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)
@@ -248,6 +337,8 @@ class Events(object):
yield self._p.publish(client, service, node, items=[item_elt])
+ if register:
+ 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):
@@ -271,6 +362,40 @@ class Events(object):
new_data[k] = v
yield self.eventCreate(client, new_timestamp, new_data, service, node, id_)
+ def _eventsListSerialise(self, events):
+ for timestamp, data in events:
+ data['date'] = unicode(timestamp)
+ data['creator'] = C.boolConst(data.get('creator', False))
+ return [e[1] for e in events]
+
+ def _eventsList(self, service, node, profile):
+ service = jid.JID(service) if service else None
+ node = node or None
+ client = self.host.getClient(profile)
+ d = self.eventsList(client, service, node)
+ d.addCallback(self._eventsListSerialise)
+ return d
+
+ @defer.inlineCallbacks
+ def eventsList(self, client, service, node):
+ """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)
+ events = []
+ for item in items[0]:
+ try:
+ event_elt = next(item.elements(NS_EVENT, u'event'))
+ except IndexError:
+ log.error(_(u"No event found in item {item_id}").format(
+ item_id = item['id']))
+ timestamp, data = self._parseEventElt(event_elt)
+ events.append((timestamp, data))
+ defer.returnValue(events)
+
def _eventInviteeGet(self, service, node, profile_key):
service = jid.JID(service) if service else None
node = node if node else NS_EVENT
@@ -345,23 +470,99 @@ class Events(object):
for item in items:
try:
event_elt = next(item.elements(NS_EVENT, u'invitee'))
- except IndexError:
+ 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
)))
- data = {}
- for key in (u'attend', u'guests'):
- try:
- data[key] = event_elt[key]
- except KeyError:
- continue
- invitees[item['id']] = data
+ else:
+ data = {}
+ for key in (u'attend', u'guests'):
+ try:
+ data[key] = event_elt[key]
+ except KeyError:
+ continue
+ invitees[item['id']] = data
defer.returnValue(invitees)
- def _invite(self, service, node, id_=NS_EVENT, email=u'', emails_extra=None, name=u'', host_name=u'', language=u'', url_template=u'',
+ 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
+ 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
+ """
+ if self._b is None:
+ raise exceptions.FeatureNotFound(_(u'"XEP-0277" (blog) plugin is needed for this feature'))
+ if item_id is None:
+ item_id = NS_EVENT
+
+ # first we authorize our invitee to see the nodes of interest
+ yield self._p.setNodeAffiliations(client, service, node, {invitee_jid: u'member'})
+ log.debug(_(u'affiliation set on event node'))
+ dummy, event_data = yield self.eventGet(client, service, node, item_id)
+ log.debug(_(u'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(client, invitees_service, invitees_node, {invitee_jid: u'publisher'})
+ log.debug(_(u'affiliation set on invitee node'))
+ yield self._p.setNodeAffiliations(client, blog_service, blog_node, {invitee_jid: u'member'})
+ blog_items, dummy = yield self._b.mbGet(client, blog_service, blog_node, None)
+
+ for item in blog_items:
+ try:
+ comments_service = jid.JID(item['comments_service'])
+ comments_node = item['comments_node']
+ except KeyError:
+ log.debug(u"no comment service set for item {item_id}".format(item_id=item['id']))
+ else:
+ yield self._p.setNodeAffiliations(client, comments_service, comments_node, {invitee_jid: u'publisher'})
+ log.debug(_(u'affiliation set on blog and comments nodes'))
+
+ # now we send the invitation
+ self.sendMessageInvitation(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):
client = self.host.getClient(profile_key)
kwargs = {u'profile': client.profile,
@@ -370,14 +571,14 @@ class Events(object):
for key in ("email", "name", "host_name", "language", "url_template", "message_subject", "message_body"):
value = locals()[key]
kwargs[key] = unicode(value)
- return self.invite(client,
- jid.JID(service) if service else None,
- node,
- id_ or NS_EVENT,
- **kwargs)
+ return self.inviteByEmail(client,
+ jid.JID(service) if service else None,
+ node,
+ id_ or NS_EVENT,
+ **kwargs)
@defer.inlineCallbacks
- def invite(self, client, service, node, id_=NS_EVENT, **kwargs):
+ 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
@@ -388,33 +589,51 @@ class Events(object):
raise exceptions.FeatureNotFound(_(u'"Invitations" plugin is needed for this feature'))
if self._b is None:
raise exceptions.FeatureNotFound(_(u'"XEP-0277" (blog) plugin is needed for this feature'))
- event_service = (service or client.jid.userhostJID())
- event_uri = uri_parse.buildXMPPUri('pubsub',
- path=event_service.full(),
+ service = service or client.jid.userhostJID()
+ event_uri = xmpp_uri.buildXMPPUri('pubsub',
+ path=service.full(),
node=node,
item=id_)
kwargs['extra'] = {u'event_uri': event_uri}
invitation_data = yield self._i.create(**kwargs)
invitee_jid = invitation_data[u'jid']
log.debug(_(u'invitation created'))
- yield self._p.setNodeAffiliations(client, event_service, node, {invitee_jid: u'member'})
- log.debug(_(u'affiliation set on event node'))
- dummy, event_data = yield self.eventGet(client, service, node, id_)
- log.debug(_(u'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(client, invitees_service, invitees_node, {invitee_jid: u'publisher'})
- log.debug(_(u'affiliation set on invitee node'))
- yield self._p.setNodeAffiliations(client, blog_service, blog_node, {invitee_jid: u'member'})
- # FIXME: what follow is crazy, we have no good way to handle comments affiliations for blog
- blog_items, dummy = yield self._b.mbGet(client, blog_service, blog_node, None)
+ # now that we have a jid, we can send normal invitation
+ yield self.invite(client, invitee_jid, service, node, id_)
- for item in blog_items:
- comments_service = jid.JID(item['comments_service'])
- comments_node = item['comments_node']
- yield self._p.setNodeAffiliations(client, comments_service, comments_node, {invitee_jid: u'publisher'})
- log.debug(_(u'affiliation set on blog and comments nodes'))
+ @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)
+
+ 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=''):
+ return []
diff --git a/sat_frontends/jp/cmd_event.py b/sat_frontends/jp/cmd_event.py
index 85c08843..f15ef8b9 100644
--- a/sat_frontends/jp/cmd_event.py
+++ b/sat_frontends/jp/cmd_event.py
@@ -383,7 +383,7 @@ class InviteeInvite(base.CommandBase):
email = self.args.email[0] if self.args.email else None
emails_extra = self.args.email[1:]
- self.host.bridge.eventInvite(
+ self.host.bridge.eventInviteByEmail(
self.args.service,
self.args.node,
self.args.item,