# HG changeset patch # User Goffi # Date 1529536904 -7200 # Node ID 1cc88adb514276d5bc1857fd9b3a2d8c64a4070e # Parent b4ecbcc2fd08d4a220f92426bd9c98bf9ba475a4 plugin events: invitations improvments + personal list - added invitation by 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 diff -r b4ecbcc2fd08 -r 1cc88adb5142 sat/plugins/plugin_exp_events.py --- a/sat/plugins/plugin_exp_events.py Thu Jun 21 01:21:44 2018 +0200 +++ b/sat/plugins/plugin_exp_events.py Thu Jun 21 01:21:44 2018 +0200 @@ -23,12 +23,17 @@ 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 @@ 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 @@ 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 @@ 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_) - - @defer.inlineCallbacks - def eventGet(self, client, service, node, id_=NS_EVENT): - """Retrieve event data + def getHandler(self, client): + return EventsHandler(self) - @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 + def _parseEventElt(self, event_elt): + """Helper method to parse event element + + @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 @@ 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 @@ 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 + + @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 if node else NS_EVENT + 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 @@ - 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 -r b4ecbcc2fd08 -r 1cc88adb5142 sat_frontends/jp/cmd_event.py --- a/sat_frontends/jp/cmd_event.py Thu Jun 21 01:21:44 2018 +0200 +++ b/sat_frontends/jp/cmd_event.py Thu Jun 21 01:21:44 2018 +0200 @@ -383,7 +383,7 @@ 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,