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
This commit is contained in:
parent
1ccb54e571
commit
29b2a7cb11
4 changed files with 386 additions and 134 deletions
175
sat/plugins/plugin_exp_events.py
Executable file → Normal file
175
sat/plugins/plugin_exp_events.py
Executable file → Normal file
|
|
@ -17,12 +17,11 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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 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
|
||||
|
||||
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 @@ class Events(object):
|
|||
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 @@ class Events(object):
|
|||
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 @@ class Events(object):
|
|||
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 @@ class Events(object):
|
|||
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 @@ class Events(object):
|
|||
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 @@ class Events(object):
|
|||
@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 @@ class Events(object):
|
|||
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 @@ class Events(object):
|
|||
invitees[item["id"]] = data
|
||||
defer.returnValue(invitees)
|
||||
|
||||
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
|
||||
|
|
@ -644,23 +588,14 @@ class Events(object):
|
|||
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 @@ class Events(object):
|
|||
# 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 @@ class EventsHandler(XMPPHandler):
|
|||
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=""):
|
||||
|
|
|
|||
187
sat/plugins/plugin_exp_invitation.py
Normal file
187
sat/plugins/plugin_exp_invitation.py
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from sat.core.i18n import _
|
||||
from sat.core import exceptions
|
||||
from sat.core.constants import Const as C
|
||||
from sat.core.log import getLogger
|
||||
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 <message> 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 []
|
||||
154
sat/plugins/plugin_exp_list_of_interest.py
Normal file
154
sat/plugins/plugin_exp_list_of_interest.py
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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 []
|
||||
409
sat/plugins/plugin_misc_email_invitation.py
Normal file
409
sat/plugins/plugin_misc_email_invitation.py
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
Loading…
Reference in a new issue