# HG changeset patch # User Goffi # Date 1555222911 -7200 # Node ID a3faf1c865961d59ac4a45d84f6f6c081d62967c # Parent cd391ea847cb7305b1a6f33639b83b59d0d13299 plugin events: refactored invitation and personal lists logic: - invitation logic has been moved to a new generic "plugin_exp_invitation" plugin - plugin_misc_invitations has be rename "plugin_exp_email_invitation" to avoid confusion - personal event list has be refactored to use a new experimental "list of interest", which regroup all interestings items, events or other ones diff -r cd391ea847cb -r a3faf1c86596 sat/plugins/plugin_exp_events.py --- a/sat/plugins/plugin_exp_events.py Sun Apr 14 08:21:51 2019 +0200 +++ b/sat/plugins/plugin_exp_events.py Sun Apr 14 08:21:51 2019 +0200 @@ -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 wokkel import disco, iwokkel from zope.interface import implements from twisted.words.protocols.jabber.xmlstream import XMPPHandler +from wokkel import pubsub -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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ @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 @@ 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 @@ 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 @@ 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 @@ # 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 @@ 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 -r cd391ea847cb -r a3faf1c86596 sat/plugins/plugin_exp_invitation.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat/plugins/plugin_exp_invitation.py Sun Apr 14 08:21:51 2019 +0200 @@ -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 -r cd391ea847cb -r a3faf1c86596 sat/plugins/plugin_exp_list_of_interest.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat/plugins/plugin_exp_list_of_interest.py Sun Apr 14 08:21:51 2019 +0200 @@ -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 -r cd391ea847cb -r a3faf1c86596 sat/plugins/plugin_misc_email_invitation.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat/plugins/plugin_misc_email_invitation.py Sun Apr 14 08:21:51 2019 +0200 @@ -0,0 +1,409 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# SAT plugin for file tansfer +# 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 . + +import shortuuid +from twisted.internet import defer +from twisted.words.protocols.jabber import jid +from twisted.words.protocols.jabber import error +from sat.core.i18n import _, D_ +from sat.core.constants import Const as C +from sat.core import exceptions +from sat.core.log import getLogger +from sat.tools import utils +from sat.tools.common import data_format +from sat.memory import persistent +from sat.tools import email as sat_email + +log = getLogger(__name__) + + +PLUGIN_INFO = { + C.PI_NAME: "Invitations", + C.PI_IMPORT_NAME: "EMAIL_INVITATION", + C.PI_TYPE: C.PLUG_TYPE_MISC, + C.PI_DEPENDENCIES: ['XEP-0077'], + C.PI_RECOMMENDATIONS: ["IDENTITY"], + C.PI_MAIN: "InvitationsPlugin", + C.PI_HANDLER: "no", + C.PI_DESCRIPTION: _(u"""invitation of people without XMPP account""") +} + + +SUFFIX_MAX = 5 +INVITEE_PROFILE_TPL = u"guest@@{uuid}" +KEY_ID = u'id' +KEY_JID = u'jid' +KEY_CREATED = u'created' +KEY_LAST_CONNECTION = u'last_connection' +KEY_GUEST_PROFILE = u'guest_profile' +KEY_PASSWORD = u'password' +KEY_EMAILS_EXTRA = u'emails_extra' +EXTRA_RESERVED = {KEY_ID, KEY_JID, KEY_CREATED, u'jid_', u'jid', KEY_LAST_CONNECTION, + KEY_GUEST_PROFILE, KEY_PASSWORD, KEY_EMAILS_EXTRA} +DEFAULT_SUBJECT = D_(u"You have been invited by {host_name} to {app_name}") +DEFAULT_BODY = D_(u"""Hello {name}! + +You have received an invitation from {host_name} to participate to "{app_name}". +To join, you just have to click on the following URL: +{url} + +Please note that this URL should not be shared with anybody! +If you want more details on {app_name}, you can check {app_url}. + +Welcome! +""") + + +class InvitationsPlugin(object): + + def __init__(self, host): + log.info(_(u"plugin Invitations initialization")) + self.host = host + self.invitations = persistent.LazyPersistentBinaryDict(u'invitations') + host.bridge.addMethod("invitationCreate", ".plugin", in_sign='sasssssssssa{ss}s', + out_sign='a{ss}', + method=self._create, + async=True) + host.bridge.addMethod("invitationGet", ".plugin", in_sign='s', out_sign='a{ss}', + method=self.get, + async=True) + host.bridge.addMethod("invitationModify", ".plugin", in_sign='sa{ss}b', + out_sign='', + method=self._modify, + async=True) + host.bridge.addMethod("invitationList", ".plugin", in_sign='s', + out_sign='a{sa{ss}}', + method=self._list, + async=True) + + def checkExtra(self, extra): + if EXTRA_RESERVED.intersection(extra): + raise ValueError( + _(u"You can't use following key(s) in extra, they are reserved: {}") + .format(u', '.join(EXTRA_RESERVED.intersection(extra)))) + + def _create(self, email=u'', emails_extra=None, jid_=u'', password=u'', name=u'', + host_name=u'', language=u'', url_template=u'', message_subject=u'', + message_body=u'', extra=None, profile=u''): + # XXX: we don't use **kwargs here to keep arguments name for introspection with + # D-Bus bridge + if emails_extra is None: + emails_extra = [] + + if extra is None: + extra = {} + else: + extra = {unicode(k): unicode(v) for k,v in extra.iteritems()} + + kwargs = {"extra": extra, + KEY_EMAILS_EXTRA: [unicode(e) for e in emails_extra] + } + + # we need to be sure that values are unicode, else they won't be pickled correctly + # with D-Bus + for key in ("jid_", "password", "name", "host_name", "email", "language", + "url_template", "message_subject", "message_body", "profile"): + value = locals()[key] + if value: + kwargs[key] = unicode(value) + d = self.create(**kwargs) + def serialize(data): + data[KEY_JID] = data[KEY_JID].full() + return data + d.addCallback(serialize) + return d + + @defer.inlineCallbacks + def create(self, **kwargs): + ur"""Create an invitation + + This will create an XMPP account and a profile, and use a UUID to retrieve them. + The profile is automatically generated in the form guest@@[UUID], this way they + can be retrieved easily + **kwargs: keywords arguments which can have the following keys, unset values are + equivalent to None: + jid_(jid.JID, None): jid to use for invitation, the jid will be created using + XEP-0077 + if the jid has no user part, an anonymous account will be used (no XMPP + account created in this case) + if None, automatically generate an account name (in the form + "invitation-[random UUID]@domain.tld") (note that this UUID is not the + same as the invitation one, as jid can be used publicly (leaking the + UUID), and invitation UUID give access to account. + in case of conflict, a suffix number is added to the account until a free + one if found (with a failure if SUFFIX_MAX is reached) + password(unicode, None): password to use (will be used for XMPP account and + profile) + None to automatically generate one + name(unicode, None): name of the invitee + will be set as profile identity if present + host_name(unicode, None): name of the host + email(unicode, None): email to send the invitation to + if None, no invitation email is sent, you can still associate email using + extra + if email is used, extra can't have "email" key + language(unicode): language of the invitee (used notabily to translate the + invitation) + TODO: not used yet + url_template(unicode, None): template to use to construct the invitation URL + use {uuid} as a placeholder for identifier + use None if you don't want to include URL (or if it is already specified + in custom message) + /!\ you must put full URL, don't forget https:// + /!\ the URL will give access to the invitee account, you should warn in + message to not publish it publicly + message_subject(unicode, None): customised message body for the invitation + email + None to use default subject + uses the same substitution as for message_body + message_body(unicode, None): customised message body for the invitation email + None to use default body + use {name} as a place holder for invitee name + use {url} as a placeholder for the invitation url + use {uuid} as a placeholder for the identifier + use {app_name} as a placeholder for this software name + use {app_url} as a placeholder for this software official website + use {profile} as a placeholder for host's profile + use {host_name} as a placeholder for host's name + extra(dict, None): extra data to associate with the invitee + some keys are reserved: + - created (creation date) + if email argument is used, "email" key can't be used + profile(unicode, None): profile of the host (person who is inviting) + @return (dict[unicode, unicode]): dictionary with: + - UUID associated with the invitee (key: id) + - filled extra dictionary, as saved in the databae + """ + ## initial checks + extra = kwargs.pop('extra', {}) + if set(kwargs).intersection(extra): + raise ValueError( + _(u"You can't use following key(s) in both args and extra: {}").format( + u', '.join(set(kwargs).intersection(extra)))) + + self.checkExtra(extra) + + email = kwargs.pop(u'email', None) + emails_extra = kwargs.pop(u'emails_extra', []) + if not email and emails_extra: + raise ValueError( + _(u'You need to provide a main email address before using emails_extra')) + + if (email is not None + and not 'url_template' in kwargs + and not 'message_body' in kwargs): + raise ValueError( + _(u"You need to provide url_template if you use default message body")) + + ## uuid + log.info(_(u"creating an invitation")) + id_ = unicode(shortuuid.uuid()) + + ## XMPP account creation + password = kwargs.pop(u'password', None) + if password is None: + password = utils.generatePassword() + assert password + # XXX: password is here saved in clear in database + # it is needed for invitation as the same password is used for profile + # and SàT need to be able to automatically open the profile with the uuid + # FIXME: we could add an extra encryption key which would be used with the uuid + # when the invitee is connecting (e.g. with URL). This key would not be + # saved and could be used to encrypt profile password. + extra[KEY_PASSWORD] = password + + jid_ = kwargs.pop(u'jid_', None) + if not jid_: + domain = self.host.memory.getConfig(None, 'xmpp_domain') + if not domain: + # TODO: fallback to profile's domain + raise ValueError(_(u"You need to specify xmpp_domain in sat.conf")) + jid_ = u"invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(), + domain=domain) + jid_ = jid.JID(jid_) + if jid_.user: + # we don't register account if there is no user as anonymous login is then + # used + try: + yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password) + except error.StanzaError as e: + prefix = jid_.user + idx = 0 + while e.condition == u'conflict': + if idx >= SUFFIX_MAX: + raise exceptions.ConflictError(_(u"Can't create XMPP account")) + jid_.user = prefix + '_' + unicode(idx) + log.info(_(u"requested jid already exists, trying with {}".format( + jid_.full()))) + try: + yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, + password) + except error.StanzaError as e: + idx += 1 + else: + break + if e.condition != u'conflict': + raise e + + log.info(_(u"account {jid_} created").format(jid_=jid_.full())) + + ## profile creation + + extra[KEY_GUEST_PROFILE] = guest_profile = INVITEE_PROFILE_TPL.format(uuid=id_) + # profile creation should not fail as we generate unique name ourselves + yield self.host.memory.createProfile(guest_profile, password) + yield self.host.memory.startSession(password, guest_profile) + yield self.host.memory.setParam("JabberID", jid_.full(), "Connection", + profile_key=guest_profile) + yield self.host.memory.setParam("Password", password, "Connection", + profile_key=guest_profile) + name = kwargs.pop(u'name', None) + if name is not None: + extra[u'name'] = name + try: + id_plugin = self.host.plugins[u'IDENTITY'] + except KeyError: + pass + else: + yield self.host.connect(guest_profile, password) + guest_client = self.host.getClient(guest_profile) + yield id_plugin.setIdentity(guest_client, {u'nick': name}) + yield self.host.disconnect(guest_profile) + + ## email + language = kwargs.pop(u'language', None) + if language is not None: + extra[u'language'] = language.strip() + + if email is not None: + extra[u'email'] = email + data_format.iter2dict(KEY_EMAILS_EXTRA, extra) + url_template = kwargs.pop(u'url_template', '') + format_args = { + u'uuid': id_, + u'app_name': C.APP_NAME, + u'app_url': C.APP_URL} + + if name is None: + format_args[u'name'] = email + else: + format_args[u'name'] = name + + profile = kwargs.pop(u'profile', None) + if profile is None: + format_args[u'profile'] = u'' + else: + format_args[u'profile'] = extra[u'profile'] = profile + + host_name = kwargs.pop(u'host_name', None) + if host_name is None: + format_args[u'host_name'] = profile or _(u"somebody") + else: + format_args[u'host_name'] = extra[u'host_name'] = host_name + + invite_url = url_template.format(**format_args) + format_args[u'url'] = invite_url + + yield sat_email.sendEmail( + self.host, + [email] + emails_extra, + (kwargs.pop(u'message_subject', None) or DEFAULT_SUBJECT).format( + **format_args), + (kwargs.pop(u'message_body', None) or DEFAULT_BODY).format(**format_args), + ) + + ## extra data saving + self.invitations[id_] = extra + + if kwargs: + log.warning(_(u"Not all arguments have been consumed: {}").format(kwargs)) + + extra[KEY_ID] = id_ + extra[KEY_JID] = jid_ + defer.returnValue(extra) + + def get(self, id_): + """Retrieve invitation linked to uuid if it exists + + @param id_(unicode): UUID linked to an invitation + @return (dict[unicode, unicode]): data associated to the invitation + @raise KeyError: there is not invitation with this id_ + """ + return self.invitations[id_] + + def _modify(self, id_, new_extra, replace): + return self.modify(id_, {unicode(k): unicode(v) for k,v in new_extra.iteritems()}, + replace) + + def modify(self, id_, new_extra, replace=False): + """Modify invitation data + + @param id_(unicode): UUID linked to an invitation + @param new_extra(dict[unicode, unicode]): data to update + empty values will be deleted if replace is True + @param replace(bool): if True replace the data + else update them + @raise KeyError: there is not invitation with this id_ + """ + self.checkExtra(new_extra) + def gotCurrentData(current_data): + if replace: + new_data = new_extra + for k in EXTRA_RESERVED: + try: + new_data[k] = current_data[k] + except KeyError: + continue + else: + new_data = current_data + for k,v in new_extra.iteritems(): + if k in EXTRA_RESERVED: + log.warning(_(u"Skipping reserved key {key}".format(k))) + continue + if v: + new_data[k] = v + else: + try: + del new_data[k] + except KeyError: + pass + + self.invitations[id_] = new_data + + d = self.invitations[id_] + d.addCallback(gotCurrentData) + return d + + def _list(self, profile=C.PROF_KEY_NONE): + return self.list(profile) + + @defer.inlineCallbacks + def list(self, profile=C.PROF_KEY_NONE): + """List invitations + + @param profile(unicode): return invitation linked to this profile only + C.PROF_KEY_NONE: don't filter invitations + @return list(unicode): invitations uids + """ + invitations = yield self.invitations.items() + if profile != C.PROF_KEY_NONE: + invitations = {id_:data for id_, data in invitations.iteritems() + if data.get(u'profile') == profile} + + defer.returnValue(invitations) diff -r cd391ea847cb -r a3faf1c86596 sat/plugins/plugin_misc_invitations.py --- a/sat/plugins/plugin_misc_invitations.py Sun Apr 14 08:21:51 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,409 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# SAT plugin for file tansfer -# 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 . - -import shortuuid -from twisted.internet import defer -from twisted.words.protocols.jabber import jid -from twisted.words.protocols.jabber import error -from sat.core.i18n import _, D_ -from sat.core.constants import Const as C -from sat.core import exceptions -from sat.core.log import getLogger -from sat.tools import utils -from sat.tools.common import data_format -from sat.memory import persistent -from sat.tools import email as sat_email - -log = getLogger(__name__) - - -PLUGIN_INFO = { - C.PI_NAME: "Invitations", - C.PI_IMPORT_NAME: "INVITATIONS", - C.PI_TYPE: C.PLUG_TYPE_MISC, - C.PI_DEPENDENCIES: ['XEP-0077'], - C.PI_RECOMMENDATIONS: ["IDENTITY"], - C.PI_MAIN: "InvitationsPlugin", - C.PI_HANDLER: "no", - C.PI_DESCRIPTION: _(u"""invitation of people without XMPP account""") -} - - -SUFFIX_MAX = 5 -INVITEE_PROFILE_TPL = u"guest@@{uuid}" -KEY_ID = u'id' -KEY_JID = u'jid' -KEY_CREATED = u'created' -KEY_LAST_CONNECTION = u'last_connection' -KEY_GUEST_PROFILE = u'guest_profile' -KEY_PASSWORD = u'password' -KEY_EMAILS_EXTRA = u'emails_extra' -EXTRA_RESERVED = {KEY_ID, KEY_JID, KEY_CREATED, u'jid_', u'jid', KEY_LAST_CONNECTION, - KEY_GUEST_PROFILE, KEY_PASSWORD, KEY_EMAILS_EXTRA} -DEFAULT_SUBJECT = D_(u"You have been invited by {host_name} to {app_name}") -DEFAULT_BODY = D_(u"""Hello {name}! - -You have received an invitation from {host_name} to participate to "{app_name}". -To join, you just have to click on the following URL: -{url} - -Please note that this URL should not be shared with anybody! -If you want more details on {app_name}, you can check {app_url}. - -Welcome! -""") - - -class InvitationsPlugin(object): - - def __init__(self, host): - log.info(_(u"plugin Invitations initialization")) - self.host = host - self.invitations = persistent.LazyPersistentBinaryDict(u'invitations') - host.bridge.addMethod("invitationCreate", ".plugin", in_sign='sasssssssssa{ss}s', - out_sign='a{ss}', - method=self._create, - async=True) - host.bridge.addMethod("invitationGet", ".plugin", in_sign='s', out_sign='a{ss}', - method=self.get, - async=True) - host.bridge.addMethod("invitationModify", ".plugin", in_sign='sa{ss}b', - out_sign='', - method=self._modify, - async=True) - host.bridge.addMethod("invitationList", ".plugin", in_sign='s', - out_sign='a{sa{ss}}', - method=self._list, - async=True) - - def checkExtra(self, extra): - if EXTRA_RESERVED.intersection(extra): - raise ValueError( - _(u"You can't use following key(s) in extra, they are reserved: {}") - .format(u', '.join(EXTRA_RESERVED.intersection(extra)))) - - def _create(self, email=u'', emails_extra=None, jid_=u'', password=u'', name=u'', - host_name=u'', language=u'', url_template=u'', message_subject=u'', - message_body=u'', extra=None, profile=u''): - # XXX: we don't use **kwargs here to keep arguments name for introspection with - # D-Bus bridge - if emails_extra is None: - emails_extra = [] - - if extra is None: - extra = {} - else: - extra = {unicode(k): unicode(v) for k,v in extra.iteritems()} - - kwargs = {"extra": extra, - KEY_EMAILS_EXTRA: [unicode(e) for e in emails_extra] - } - - # we need to be sure that values are unicode, else they won't be pickled correctly - # with D-Bus - for key in ("jid_", "password", "name", "host_name", "email", "language", - "url_template", "message_subject", "message_body", "profile"): - value = locals()[key] - if value: - kwargs[key] = unicode(value) - d = self.create(**kwargs) - def serialize(data): - data[KEY_JID] = data[KEY_JID].full() - return data - d.addCallback(serialize) - return d - - @defer.inlineCallbacks - def create(self, **kwargs): - ur"""Create an invitation - - This will create an XMPP account and a profile, and use a UUID to retrieve them. - The profile is automatically generated in the form guest@@[UUID], this way they - can be retrieved easily - **kwargs: keywords arguments which can have the following keys, unset values are - equivalent to None: - jid_(jid.JID, None): jid to use for invitation, the jid will be created using - XEP-0077 - if the jid has no user part, an anonymous account will be used (no XMPP - account created in this case) - if None, automatically generate an account name (in the form - "invitation-[random UUID]@domain.tld") (note that this UUID is not the - same as the invitation one, as jid can be used publicly (leaking the - UUID), and invitation UUID give access to account. - in case of conflict, a suffix number is added to the account until a free - one if found (with a failure if SUFFIX_MAX is reached) - password(unicode, None): password to use (will be used for XMPP account and - profile) - None to automatically generate one - name(unicode, None): name of the invitee - will be set as profile identity if present - host_name(unicode, None): name of the host - email(unicode, None): email to send the invitation to - if None, no invitation email is sent, you can still associate email using - extra - if email is used, extra can't have "email" key - language(unicode): language of the invitee (used notabily to translate the - invitation) - TODO: not used yet - url_template(unicode, None): template to use to construct the invitation URL - use {uuid} as a placeholder for identifier - use None if you don't want to include URL (or if it is already specified - in custom message) - /!\ you must put full URL, don't forget https:// - /!\ the URL will give access to the invitee account, you should warn in - message to not publish it publicly - message_subject(unicode, None): customised message body for the invitation - email - None to use default subject - uses the same substitution as for message_body - message_body(unicode, None): customised message body for the invitation email - None to use default body - use {name} as a place holder for invitee name - use {url} as a placeholder for the invitation url - use {uuid} as a placeholder for the identifier - use {app_name} as a placeholder for this software name - use {app_url} as a placeholder for this software official website - use {profile} as a placeholder for host's profile - use {host_name} as a placeholder for host's name - extra(dict, None): extra data to associate with the invitee - some keys are reserved: - - created (creation date) - if email argument is used, "email" key can't be used - profile(unicode, None): profile of the host (person who is inviting) - @return (dict[unicode, unicode]): dictionary with: - - UUID associated with the invitee (key: id) - - filled extra dictionary, as saved in the databae - """ - ## initial checks - extra = kwargs.pop('extra', {}) - if set(kwargs).intersection(extra): - raise ValueError( - _(u"You can't use following key(s) in both args and extra: {}").format( - u', '.join(set(kwargs).intersection(extra)))) - - self.checkExtra(extra) - - email = kwargs.pop(u'email', None) - emails_extra = kwargs.pop(u'emails_extra', []) - if not email and emails_extra: - raise ValueError( - _(u'You need to provide a main email address before using emails_extra')) - - if (email is not None - and not 'url_template' in kwargs - and not 'message_body' in kwargs): - raise ValueError( - _(u"You need to provide url_template if you use default message body")) - - ## uuid - log.info(_(u"creating an invitation")) - id_ = unicode(shortuuid.uuid()) - - ## XMPP account creation - password = kwargs.pop(u'password', None) - if password is None: - password = utils.generatePassword() - assert password - # XXX: password is here saved in clear in database - # it is needed for invitation as the same password is used for profile - # and SàT need to be able to automatically open the profile with the uuid - # FIXME: we could add an extra encryption key which would be used with the uuid - # when the invitee is connecting (e.g. with URL). This key would not be - # saved and could be used to encrypt profile password. - extra[KEY_PASSWORD] = password - - jid_ = kwargs.pop(u'jid_', None) - if not jid_: - domain = self.host.memory.getConfig(None, 'xmpp_domain') - if not domain: - # TODO: fallback to profile's domain - raise ValueError(_(u"You need to specify xmpp_domain in sat.conf")) - jid_ = u"invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(), - domain=domain) - jid_ = jid.JID(jid_) - if jid_.user: - # we don't register account if there is no user as anonymous login is then - # used - try: - yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password) - except error.StanzaError as e: - prefix = jid_.user - idx = 0 - while e.condition == u'conflict': - if idx >= SUFFIX_MAX: - raise exceptions.ConflictError(_(u"Can't create XMPP account")) - jid_.user = prefix + '_' + unicode(idx) - log.info(_(u"requested jid already exists, trying with {}".format( - jid_.full()))) - try: - yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, - password) - except error.StanzaError as e: - idx += 1 - else: - break - if e.condition != u'conflict': - raise e - - log.info(_(u"account {jid_} created").format(jid_=jid_.full())) - - ## profile creation - - extra[KEY_GUEST_PROFILE] = guest_profile = INVITEE_PROFILE_TPL.format(uuid=id_) - # profile creation should not fail as we generate unique name ourselves - yield self.host.memory.createProfile(guest_profile, password) - yield self.host.memory.startSession(password, guest_profile) - yield self.host.memory.setParam("JabberID", jid_.full(), "Connection", - profile_key=guest_profile) - yield self.host.memory.setParam("Password", password, "Connection", - profile_key=guest_profile) - name = kwargs.pop(u'name', None) - if name is not None: - extra[u'name'] = name - try: - id_plugin = self.host.plugins[u'IDENTITY'] - except KeyError: - pass - else: - yield self.host.connect(guest_profile, password) - guest_client = self.host.getClient(guest_profile) - yield id_plugin.setIdentity(guest_client, {u'nick': name}) - yield self.host.disconnect(guest_profile) - - ## email - language = kwargs.pop(u'language', None) - if language is not None: - extra[u'language'] = language.strip() - - if email is not None: - extra[u'email'] = email - data_format.iter2dict(KEY_EMAILS_EXTRA, extra) - url_template = kwargs.pop(u'url_template', '') - format_args = { - u'uuid': id_, - u'app_name': C.APP_NAME, - u'app_url': C.APP_URL} - - if name is None: - format_args[u'name'] = email - else: - format_args[u'name'] = name - - profile = kwargs.pop(u'profile', None) - if profile is None: - format_args[u'profile'] = u'' - else: - format_args[u'profile'] = extra[u'profile'] = profile - - host_name = kwargs.pop(u'host_name', None) - if host_name is None: - format_args[u'host_name'] = profile or _(u"somebody") - else: - format_args[u'host_name'] = extra[u'host_name'] = host_name - - invite_url = url_template.format(**format_args) - format_args[u'url'] = invite_url - - yield sat_email.sendEmail( - self.host, - [email] + emails_extra, - (kwargs.pop(u'message_subject', None) or DEFAULT_SUBJECT).format( - **format_args), - (kwargs.pop(u'message_body', None) or DEFAULT_BODY).format(**format_args), - ) - - ## extra data saving - self.invitations[id_] = extra - - if kwargs: - log.warning(_(u"Not all arguments have been consumed: {}").format(kwargs)) - - extra[KEY_ID] = id_ - extra[KEY_JID] = jid_ - defer.returnValue(extra) - - def get(self, id_): - """Retrieve invitation linked to uuid if it exists - - @param id_(unicode): UUID linked to an invitation - @return (dict[unicode, unicode]): data associated to the invitation - @raise KeyError: there is not invitation with this id_ - """ - return self.invitations[id_] - - def _modify(self, id_, new_extra, replace): - return self.modify(id_, {unicode(k): unicode(v) for k,v in new_extra.iteritems()}, - replace) - - def modify(self, id_, new_extra, replace=False): - """Modify invitation data - - @param id_(unicode): UUID linked to an invitation - @param new_extra(dict[unicode, unicode]): data to update - empty values will be deleted if replace is True - @param replace(bool): if True replace the data - else update them - @raise KeyError: there is not invitation with this id_ - """ - self.checkExtra(new_extra) - def gotCurrentData(current_data): - if replace: - new_data = new_extra - for k in EXTRA_RESERVED: - try: - new_data[k] = current_data[k] - except KeyError: - continue - else: - new_data = current_data - for k,v in new_extra.iteritems(): - if k in EXTRA_RESERVED: - log.warning(_(u"Skipping reserved key {key}".format(k))) - continue - if v: - new_data[k] = v - else: - try: - del new_data[k] - except KeyError: - pass - - self.invitations[id_] = new_data - - d = self.invitations[id_] - d.addCallback(gotCurrentData) - return d - - def _list(self, profile=C.PROF_KEY_NONE): - return self.list(profile) - - @defer.inlineCallbacks - def list(self, profile=C.PROF_KEY_NONE): - """List invitations - - @param profile(unicode): return invitation linked to this profile only - C.PROF_KEY_NONE: don't filter invitations - @return list(unicode): invitations uids - """ - invitations = yield self.invitations.items() - if profile != C.PROF_KEY_NONE: - invitations = {id_:data for id_, data in invitations.iteritems() - if data.get(u'profile') == profile} - - defer.returnValue(invitations)