plugin events: invitations improvments + personal list

- added invitation by <message> mechanism
- added a personal list in PEP which keep events created by user, or in which she has been invited
- new bridge methods: eventsList, and eventInviteByEmail. eventInvite is now used to invite a entity by jid (not email like before)
- on invite, an invitation message is send to invitee's jid
- when an invitation message is received, event is automatically linked in personal list
- when creating a new event, event is automatically linked in personal list
This commit is contained in:
Goffi 2018-06-21 01:21:44 +02:00
commit a365c2b797
2 changed files with 294 additions and 75 deletions

View file

@ -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 <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)
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 <message> 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 []

View file

@ -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,