comparison sat/plugins/plugin_exp_events.py @ 2912:a3faf1c86596

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
author Goffi <goffi@goffi.org>
date Sun, 14 Apr 2019 08:21:51 +0200
parents 003b8b4b56a7
children b256e90612d0
comparison
equal deleted inserted replaced
2911:cd391ea847cb 2912:a3faf1c86596
15 # GNU Affero General Public License for more details. 15 # GNU Affero General Public License for more details.
16 16
17 # You should have received a copy of the GNU Affero General Public License 17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 import shortuuid
20 from sat.core.i18n import _ 21 from sat.core.i18n import _
21 from sat.core import exceptions 22 from sat.core import exceptions
22 from sat.core.constants import Const as C 23 from sat.core.constants import Const as C
23 from sat.core.log import getLogger 24 from sat.core.log import getLogger
24
25 log = getLogger(__name__)
26 from sat.tools import utils 25 from sat.tools import utils
27 from sat.tools.common import uri as xmpp_uri 26 from sat.tools.common import uri as xmpp_uri
28 from sat.tools.common import date_utils 27 from sat.tools.common import date_utils
29 from twisted.internet import defer 28 from twisted.internet import defer
30 from twisted.words.protocols.jabber import jid, error 29 from twisted.words.protocols.jabber import jid, error
31 from twisted.words.xish import domish 30 from twisted.words.xish import domish
32 from wokkel import disco, iwokkel 31 from wokkel import disco, iwokkel
33 from zope.interface import implements 32 from zope.interface import implements
34 from twisted.words.protocols.jabber.xmlstream import XMPPHandler 33 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
35
36 from wokkel import pubsub 34 from wokkel import pubsub
37 import shortuuid 35
36 log = getLogger(__name__)
38 37
39 38
40 PLUGIN_INFO = { 39 PLUGIN_INFO = {
41 C.PI_NAME: "Event plugin", 40 C.PI_NAME: "Events",
42 C.PI_IMPORT_NAME: "EVENTS", 41 C.PI_IMPORT_NAME: "EVENTS",
43 C.PI_TYPE: "EXP", 42 C.PI_TYPE: "EXP",
44 C.PI_PROTOCOLS: [], 43 C.PI_PROTOCOLS: [],
45 C.PI_DEPENDENCIES: ["XEP-0060"], 44 C.PI_DEPENDENCIES: [u"XEP-0060", u"INVITATION", u"LIST_INTEREST"],
46 C.PI_RECOMMENDATIONS: ["INVITATIONS", "XEP-0277"], 45 C.PI_RECOMMENDATIONS: ["XEP-0277", "EMAIL_INVITATION"],
47 C.PI_MAIN: "Events", 46 C.PI_MAIN: "Events",
48 C.PI_HANDLER: "yes", 47 C.PI_HANDLER: "yes",
49 C.PI_DESCRIPTION: _("""Experimental implementation of XMPP events management"""), 48 C.PI_DESCRIPTION: _(u"""Experimental implementation of XMPP events management"""),
50 } 49 }
51 50
52 NS_EVENT = "org.salut-a-toi.event:0" 51 NS_EVENT = "org.salut-a-toi.event:0"
53 NS_EVENT_LIST = NS_EVENT + "#list"
54 NS_EVENT_INVIT = NS_EVENT + "#invitation"
55 INVITATION = '/message[@type="chat"]/invitation[@xmlns="{ns_invit}"]'.format(
56 ns_invit=NS_EVENT_INVIT
57 )
58 52
59 53
60 class Events(object): 54 class Events(object):
61 """Q&D module to handle event attendance answer, experimentation only""" 55 """Q&D module to handle event attendance answer, experimentation only"""
62 56
63 def __init__(self, host): 57 def __init__(self, host):
64 log.info(_(u"Event plugin initialization")) 58 log.info(_(u"Event plugin initialization"))
65 self.host = host 59 self.host = host
66 self._p = self.host.plugins["XEP-0060"] 60 self._p = self.host.plugins["XEP-0060"]
67 self._i = self.host.plugins.get("INVITATIONS") 61 self._i = self.host.plugins.get("EMAIL_INVITATION")
68 self._b = self.host.plugins.get("XEP-0277") 62 self._b = self.host.plugins.get("XEP-0277")
63 self.host.plugins[u"INVITATION"].registerNamespace(NS_EVENT,
64 self.register)
69 host.bridge.addMethod( 65 host.bridge.addMethod(
70 "eventGet", 66 "eventGet",
71 ".plugin", 67 ".plugin",
72 in_sign="ssss", 68 in_sign="ssss",
73 out_sign="(ia{ss})", 69 out_sign="(ia{ss})",
227 if not id_: 223 if not id_:
228 id_ = NS_EVENT 224 id_ = NS_EVENT
229 items, metadata = yield self._p.getItems(client, service, node, item_ids=[id_]) 225 items, metadata = yield self._p.getItems(client, service, node, item_ids=[id_])
230 try: 226 try:
231 event_elt = next(items[0].elements(NS_EVENT, u"event")) 227 event_elt = next(items[0].elements(NS_EVENT, u"event"))
228 except StopIteration:
229 raise exceptions.NotFound(_(u"No event element has been found"))
232 except IndexError: 230 except IndexError:
233 raise exceptions.NotFound(_(u"No event with this id has been found")) 231 raise exceptions.NotFound(_(u"No event with this id has been found"))
234 defer.returnValue(event_elt) 232 defer.returnValue(event_elt)
235 233
236 @defer.inlineCallbacks
237 def register(self, client, service, node, event_id, event_elt, creator=False): 234 def register(self, client, service, node, event_id, event_elt, creator=False):
238 """register evenement in personal events list 235 """register evenement in personal events list
239 236
240 @param service(jid.JID): pubsub service of the event 237 @param service(jid.JID): pubsub service of the event
241 @param node(unicode): event node 238 @param node(unicode): event node
242 @param event_id(unicode): event id 239 @param event_id(unicode): event id
243 @param event_elt(domish.Element): event element 240 @param event_elt(domish.Element): event element
244 note that this element will be modified in place 241 note that this element will be modified in place
245 @param creator(bool): True if client's profile is the creator of the node 242 @param creator(bool): True if client's profile is the creator of the node
246 """ 243 """
247 # we save a link to the event in our local list 244 link_elt = event_elt.addElement("link")
248 try:
249 # TODO: check auto-create, no need to create node first if available
250 options = {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_WHITELIST}
251 yield self._p.createNode(
252 client,
253 client.jid.userhostJID(),
254 nodeIdentifier=NS_EVENT_LIST,
255 options=options,
256 )
257 except error.StanzaError as e:
258 if e.condition == u"conflict":
259 log.debug(_(u"requested node already exists"))
260 link_elt = event_elt.addElement((NS_EVENT_LIST, "link"))
261 link_elt["service"] = service.full() 245 link_elt["service"] = service.full()
262 link_elt["node"] = node 246 link_elt["node"] = node
263 link_elt["item"] = event_id 247 link_elt["item"] = event_id
264 item_id = xmpp_uri.buildXMPPUri( 248 return self.host.plugins[u'LIST_INTEREST'].registerPubsub(
265 u"pubsub", path=service.full(), node=node, item=event_id 249 client, NS_EVENT, service, node, event_id, creator,
266 ) 250 element=event_elt)
267 if creator:
268 event_elt["creator"] = "true"
269 item_elt = pubsub.Item(id=item_id, payload=event_elt)
270 yield self._p.publish(
271 client, client.jid.userhostJID(), NS_EVENT_LIST, items=[item_elt]
272 )
273 251
274 def _eventGet(self, service, node, id_=u"", profile_key=C.PROF_KEY_NONE): 252 def _eventGet(self, service, node, id_=u"", profile_key=C.PROF_KEY_NONE):
275 service = jid.JID(service) if service else None 253 service = jid.JID(service) if service else None
276 node = node if node else NS_EVENT 254 node = node if node else NS_EVENT
277 client = self.host.getClient(profile_key) 255 client = self.host.getClient(profile_key)
394 372
395 if register: 373 if register:
396 yield self.register(client, service, node, event_id, event_elt, creator=True) 374 yield self.register(client, service, node, event_id, event_elt, creator=True)
397 defer.returnValue(node) 375 defer.returnValue(node)
398 376
399 def _eventModify( 377 def _eventModify(self, service, node, id_, timestamp_update, data_update,
400 self, 378 profile_key=C.PROF_KEY_NONE):
401 service, 379 service = jid.JID(service) if service else None
402 node, 380 if not node:
403 id_, 381 raise ValueError(_(u"missing node"))
404 timestamp_update,
405 data_update,
406 profile_key=C.PROF_KEY_NONE,
407 ):
408 service = jid.JID(service) if service else None
409 node = node if node else NS_EVENT
410 client = self.host.getClient(profile_key) 382 client = self.host.getClient(profile_key)
411 return self.eventModify( 383 return self.eventModify(
412 client, service, node, id_ or NS_EVENT, timestamp_update or None, data_update 384 client, service, node, id_ or NS_EVENT, timestamp_update or None, data_update
413 ) 385 )
414 386
442 d = self.eventsList(client, service, node) 414 d = self.eventsList(client, service, node)
443 d.addCallback(self._eventsListSerialise) 415 d.addCallback(self._eventsListSerialise)
444 return d 416 return d
445 417
446 @defer.inlineCallbacks 418 @defer.inlineCallbacks
447 def eventsList(self, client, service, node): 419 def eventsList(self, client, service, node=None):
448 """Retrieve list of registered events 420 """Retrieve list of registered events
449 421
450 @return list(tuple(int, dict)): list of events (timestamp + metadata) 422 @return list(tuple(int, dict)): list of events (timestamp + metadata)
451 """ 423 """
452 if not node: 424 items, metadata = yield self.host.plugins[u'LIST_INTEREST'].listInterests(
453 node = NS_EVENT_LIST 425 client, service, node, namespace=NS_EVENT)
454 items = yield self._p.getItems(client, service, node)
455 events = [] 426 events = []
456 for item in items[0]: 427 for item in items:
457 try: 428 try:
458 event_elt = next(item.elements(NS_EVENT, u"event")) 429 event_elt = next(item.interest.pubsub.elements(NS_EVENT, u"event"))
459 except IndexError: 430 except IndexError:
460 log.error( 431 log.warning(
461 _(u"No event found in item {item_id}").format(item_id=item["id"]) 432 _(u"No event found in item {item_id}").format(item_id=item["id"])
462 ) 433 )
463 timestamp, data = self._parseEventElt(event_elt) 434 else:
464 events.append((timestamp, data)) 435 timestamp, data = self._parseEventElt(event_elt)
436 events.append((timestamp, data))
465 defer.returnValue(events) 437 defer.returnValue(events)
466 438
467 def _eventInviteeGet(self, service, node, profile_key): 439 def _eventInviteeGet(self, service, node, profile_key):
468 service = jid.JID(service) if service else None 440 service = jid.JID(service) if service else None
469 node = node if node else NS_EVENT 441 node = node if node else NS_EVENT
477 @param service(unicode, None): PubSub service 449 @param service(unicode, None): PubSub service
478 @param node(unicode): PubSub node of the event 450 @param node(unicode): PubSub node of the event
479 @return (dict): a dict with current attendance status, 451 @return (dict): a dict with current attendance status,
480 an empty dict is returned if nothing has been answered yed 452 an empty dict is returned if nothing has been answered yed
481 """ 453 """
482 items, metadata = yield self._p.getItems(
483 client, service, node, item_ids=[client.jid.userhost()]
484 )
485 try: 454 try:
455 items, metadata = yield self._p.getItems(
456 client, service, node, item_ids=[client.jid.userhost()]
457 )
486 event_elt = next(items[0].elements(NS_EVENT, u"invitee")) 458 event_elt = next(items[0].elements(NS_EVENT, u"invitee"))
487 except IndexError: 459 except (exceptions.NotFound, IndexError):
488 # no item found, event data are not set yet 460 # no item found, event data are not set yet
489 defer.returnValue({}) 461 defer.returnValue({})
490 data = {} 462 data = {}
491 for key in (u"attend", u"guests"): 463 for key in (u"attend", u"guests"):
492 try: 464 try:
540 for item in items: 512 for item in items:
541 try: 513 try:
542 event_elt = next(item.elements(NS_EVENT, u"invitee")) 514 event_elt = next(item.elements(NS_EVENT, u"invitee"))
543 except StopIteration: 515 except StopIteration:
544 # no item found, event data are not set yet 516 # no item found, event data are not set yet
545 log.warning( 517 log.warning(_(
546 _( 518 u"no data found for {item_id} (service: {service}, node: {node})"
547 u"no data found for {item_id} (service: {service}, node: {node})".format( 519 .format(item_id=item["id"], service=service, node=node)))
548 item_id=item["id"], service=service, node=node
549 )
550 )
551 )
552 else: 520 else:
553 data = {} 521 data = {}
554 for key in (u"attend", u"guests"): 522 for key in (u"attend", u"guests"):
555 try: 523 try:
556 data[key] = event_elt[key] 524 data[key] = event_elt[key]
557 except KeyError: 525 except KeyError:
558 continue 526 continue
559 invitees[item["id"]] = data 527 invitees[item["id"]] = data
560 defer.returnValue(invitees) 528 defer.returnValue(invitees)
561
562 def sendMessageInvitation(self, client, invitee_jid, service, node, item_id):
563 """Send an invitation in a <message> stanza
564
565 @param invitee_jid(jid.JID): entitee to send invitation to
566 @param service(jid.JID): pubsub service of the event
567 @param node(unicode): node of the event
568 @param item_id(unicode): id of the event
569 """
570 mess_data = {
571 "from": client.jid,
572 "to": invitee_jid,
573 "uid": "",
574 "message": {},
575 "type": C.MESS_TYPE_CHAT,
576 "subject": {},
577 "extra": {},
578 }
579 client.generateMessageXML(mess_data)
580 event_elt = mess_data["xml"].addElement("invitation", NS_EVENT_INVIT)
581 event_elt["service"] = service.full()
582 event_elt["node"] = node
583 event_elt["item"] = item_id
584 client.send(mess_data["xml"])
585 529
586 def _invite(self, invitee_jid, service, node, item_id, profile): 530 def _invite(self, invitee_jid, service, node, item_id, profile):
587 client = self.host.getClient(profile) 531 client = self.host.getClient(profile)
588 service = jid.JID(service) if service else None 532 service = jid.JID(service) if service else None
589 node = node or None 533 node = node or None
642 client, comments_service, comments_node, {invitee_jid: u"publisher"} 586 client, comments_service, comments_node, {invitee_jid: u"publisher"}
643 ) 587 )
644 log.debug(_(u"affiliation set on blog and comments nodes")) 588 log.debug(_(u"affiliation set on blog and comments nodes"))
645 589
646 # now we send the invitation 590 # now we send the invitation
647 self.sendMessageInvitation(client, invitee_jid, service, node, item_id) 591 pubsub_invitation = self.host.plugins[u'PUBSUB_INVITATION']
648 592 pubsub_invitation.sendPubsubInvitation(client, invitee_jid, service, node,
649 def _inviteByEmail( 593 item_id)
650 self, 594
651 service, 595 def _inviteByEmail(self, service, node, id_=NS_EVENT, email=u"", emails_extra=None,
652 node, 596 name=u"", host_name=u"", language=u"", url_template=u"",
653 id_=NS_EVENT, 597 message_subject=u"", message_body=u"",
654 email=u"", 598 profile_key=C.PROF_KEY_NONE):
655 emails_extra=None,
656 name=u"",
657 host_name=u"",
658 language=u"",
659 url_template=u"",
660 message_subject=u"",
661 message_body=u"",
662 profile_key=C.PROF_KEY_NONE,
663 ):
664 client = self.host.getClient(profile_key) 599 client = self.host.getClient(profile_key)
665 kwargs = { 600 kwargs = {
666 u"profile": client.profile, 601 u"profile": client.profile,
667 u"emails_extra": [unicode(e) for e in emails_extra], 602 u"emails_extra": [unicode(e) for e in emails_extra],
668 } 603 }
706 invitee_jid = invitation_data[u"jid"] 641 invitee_jid = invitation_data[u"jid"]
707 log.debug(_(u"invitation created")) 642 log.debug(_(u"invitation created"))
708 # now that we have a jid, we can send normal invitation 643 # now that we have a jid, we can send normal invitation
709 yield self.invite(client, invitee_jid, service, node, id_) 644 yield self.invite(client, invitee_jid, service, node, id_)
710 645
711 @defer.inlineCallbacks
712 def onInvitation(self, message_elt, client):
713 invitation_elt = message_elt.invitation
714 try:
715 service = jid.JID(invitation_elt["service"])
716 node = invitation_elt["node"]
717 event_id = invitation_elt["item"]
718 except (RuntimeError, KeyError):
719 log.warning(_(u"Bad invitation: {xml}").format(xml=message_elt.toXml()))
720
721 event_elt = yield self.getEventElement(client, service, node, event_id)
722 yield self.register(client, service, node, event_id, event_elt, creator=False)
723
724 646
725 class EventsHandler(XMPPHandler): 647 class EventsHandler(XMPPHandler):
726 implements(iwokkel.IDisco) 648 implements(iwokkel.IDisco)
727 649
728 def __init__(self, plugin_parent): 650 def __init__(self, plugin_parent):
729 self.plugin_parent = plugin_parent 651 self.plugin_parent = plugin_parent
730
731 @property
732 def host(self):
733 return self.plugin_parent.host
734
735 def connectionInitialized(self):
736 self.xmlstream.addObserver(
737 INVITATION, self.plugin_parent.onInvitation, client=self.parent
738 )
739 652
740 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): 653 def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
741 return [ 654 return [
742 disco.DiscoFeature(NS_EVENT), 655 disco.DiscoFeature(NS_EVENT),
743 disco.DiscoFeature(NS_EVENT_LIST),
744 disco.DiscoFeature(NS_EVENT_INVIT),
745 ] 656 ]
746 657
747 def getDiscoItems(self, requestor, target, nodeIdentifier=""): 658 def getDiscoItems(self, requestor, target, nodeIdentifier=""):
748 return [] 659 return []