comparison sat/plugins/plugin_exp_events.py @ 3462:12dc234f698c

plugin invitation: pubsub invitations: - new Pubsub invitation plugin, to have a generic way to manage invitation on Pubsub based features - `invitePreflight` and `onInvitationPreflight` method can be implemented to customise invitation for a namespace - refactored events invitations to use the new plugin - a Pubsub invitation can now be for a whole node instead of a specific item - if invitation is for a node, a namespace can be specified to indicate what this node is about. It is then added in `extra` data - an element (domish.Element) can be added in `extra` data, it will then be added in the invitation - some code modernisation
author Goffi <goffi@goffi.org>
date Fri, 19 Feb 2021 15:50:22 +0100
parents 559a625a236b
children be6d91572633
comparison
equal deleted inserted replaced
3461:02a8d227d5bb 3462:12dc234f698c
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 from typing import Optional
20 import shortuuid 21 import shortuuid
21 from sat.core.i18n import _ 22 from sat.core.i18n import _
22 from sat.core import exceptions 23 from sat.core import exceptions
23 from sat.core.constants import Const as C 24 from sat.core.constants import Const as C
24 from sat.core.log import getLogger 25 from sat.core.log import getLogger
26 from sat.core.xmpp import SatXMPPEntity
25 from sat.tools import utils 27 from sat.tools import utils
26 from sat.tools.common import uri as xmpp_uri 28 from sat.tools.common import uri as xmpp_uri
27 from sat.tools.common import date_utils 29 from sat.tools.common import date_utils
28 from twisted.internet import defer 30 from twisted.internet import defer
29 from twisted.words.protocols.jabber import jid, error 31 from twisted.words.protocols.jabber import jid, error
39 PLUGIN_INFO = { 41 PLUGIN_INFO = {
40 C.PI_NAME: "Events", 42 C.PI_NAME: "Events",
41 C.PI_IMPORT_NAME: "EVENTS", 43 C.PI_IMPORT_NAME: "EVENTS",
42 C.PI_TYPE: "EXP", 44 C.PI_TYPE: "EXP",
43 C.PI_PROTOCOLS: [], 45 C.PI_PROTOCOLS: [],
44 C.PI_DEPENDENCIES: ["XEP-0060", "INVITATION", "LIST_INTEREST"], 46 C.PI_DEPENDENCIES: ["XEP-0060", "INVITATION", "PUBSUB_INVITATION", "LIST_INTEREST"],
45 C.PI_RECOMMENDATIONS: ["XEP-0277", "EMAIL_INVITATION"], 47 C.PI_RECOMMENDATIONS: ["XEP-0277", "EMAIL_INVITATION"],
46 C.PI_MAIN: "Events", 48 C.PI_MAIN: "Events",
47 C.PI_HANDLER: "yes", 49 C.PI_HANDLER: "yes",
48 C.PI_DESCRIPTION: _("""Experimental implementation of XMPP events management"""), 50 C.PI_DESCRIPTION: _("""Experimental implementation of XMPP events management"""),
49 } 51 }
59 self.host = host 61 self.host = host
60 self._p = self.host.plugins["XEP-0060"] 62 self._p = self.host.plugins["XEP-0060"]
61 self._i = self.host.plugins.get("EMAIL_INVITATION") 63 self._i = self.host.plugins.get("EMAIL_INVITATION")
62 self._b = self.host.plugins.get("XEP-0277") 64 self._b = self.host.plugins.get("XEP-0277")
63 self.host.registerNamespace("event", NS_EVENT) 65 self.host.registerNamespace("event", NS_EVENT)
64 self.host.plugins["INVITATION"].registerNamespace(NS_EVENT, 66 self.host.plugins["PUBSUB_INVITATION"].register(NS_EVENT, self)
65 self.register)
66 host.bridge.addMethod( 67 host.bridge.addMethod(
67 "eventGet", 68 "eventGet",
68 ".plugin", 69 ".plugin",
69 in_sign="ssss", 70 in_sign="ssss",
70 out_sign="(ia{ss})", 71 out_sign="(ia{ss})",
230 raise exceptions.NotFound(_("No event element has been found")) 231 raise exceptions.NotFound(_("No event element has been found"))
231 except IndexError: 232 except IndexError:
232 raise exceptions.NotFound(_("No event with this id has been found")) 233 raise exceptions.NotFound(_("No event with this id has been found"))
233 defer.returnValue(event_elt) 234 defer.returnValue(event_elt)
234 235
235 def register(self, client, name, extra, service, node, event_id, item_elt,
236 creator=False):
237 """Register evenement in personal events list
238
239 @param service(jid.JID): pubsub service of the event
240 @param node(unicode): event node
241 @param event_id(unicode): event id
242 @param event_elt(domish.Element): event element
243 note that this element will be modified in place
244 @param creator(bool): True if client's profile is the creator of the node
245 """
246 event_elt = item_elt.event
247 link_elt = event_elt.addElement("link")
248 link_elt["service"] = service.full()
249 link_elt["node"] = node
250 link_elt["item"] = event_id
251 __, event_data = self._parseEventElt(event_elt)
252 name = event_data.get('name')
253 if 'image' in event_data:
254 extra = {'thumb_url': event_data['image']}
255 else:
256 extra = None
257 return self.host.plugins['LIST_INTEREST'].registerPubsub(
258 client, NS_EVENT, service, node, event_id, creator,
259 name=name, element=event_elt, extra=extra)
260
261 def _eventGet(self, service, node, id_="", profile_key=C.PROF_KEY_NONE): 236 def _eventGet(self, service, node, id_="", profile_key=C.PROF_KEY_NONE):
262 service = jid.JID(service) if service else None 237 service = jid.JID(service) if service else None
263 node = node if node else NS_EVENT 238 node = node if node else NS_EVENT
264 client = self.host.getClient(profile_key) 239 client = self.host.getClient(profile_key)
265 return self.eventGet(client, service, node, id_) 240 return self.eventGet(client, service, node, id_)
287 ): 262 ):
288 service = jid.JID(service) if service else None 263 service = jid.JID(service) if service else None
289 node = node or None 264 node = node or None
290 client = self.host.getClient(profile_key) 265 client = self.host.getClient(profile_key)
291 data["register"] = C.bool(data.get("register", C.BOOL_FALSE)) 266 data["register"] = C.bool(data.get("register", C.BOOL_FALSE))
292 return self.eventCreate(client, timestamp, data, service, node, id_ or NS_EVENT) 267 return defer.ensureDeferred(
293 268 self.eventCreate(client, timestamp, data, service, node, id_ or NS_EVENT)
294 @defer.inlineCallbacks 269 )
295 def eventCreate(self, client, timestamp, data, service, node=None, event_id=NS_EVENT): 270
271 async def eventCreate(self, client, timestamp, data, service, node=None, event_id=NS_EVENT):
296 """Create or replace an event 272 """Create or replace an event
297 273
298 @param service(jid.JID, None): PubSub service 274 @param service(jid.JID, None): PubSub service
299 @param node(unicode, None): PubSub node of the event 275 @param node(unicode, None): PubSub node of the event
300 None will create instant node. 276 None will create instant node.
339 k = uri_type + "_" + to_delete 315 k = uri_type + "_" + to_delete
340 if k in data: 316 if k in data:
341 del data[k] 317 del data[k]
342 if key not in data: 318 if key not in data:
343 # FIXME: affiliate invitees 319 # FIXME: affiliate invitees
344 uri_node = yield self._p.createNode(client, service) 320 uri_node = await self._p.createNode(client, service)
345 yield self._p.setConfiguration( 321 await self._p.setConfiguration(
346 client, 322 client,
347 service, 323 service,
348 uri_node, 324 uri_node,
349 {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_WHITELIST}, 325 {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_WHITELIST},
350 ) 326 )
370 elt["name"] = key 346 elt["name"] = key
371 347
372 item_elt = pubsub.Item(id=event_id, payload=event_elt) 348 item_elt = pubsub.Item(id=event_id, payload=event_elt)
373 try: 349 try:
374 # TODO: check auto-create, no need to create node first if available 350 # TODO: check auto-create, no need to create node first if available
375 node = yield self._p.createNode(client, service, nodeIdentifier=node) 351 node = await self._p.createNode(client, service, nodeIdentifier=node)
376 except error.StanzaError as e: 352 except error.StanzaError as e:
377 if e.condition == "conflict": 353 if e.condition == "conflict":
378 log.debug(_("requested node already exists")) 354 log.debug(_("requested node already exists"))
379 355
380 yield self._p.publish(client, service, node, items=[item_elt]) 356 await self._p.publish(client, service, node, items=[item_elt])
381 357
382 if register: 358 if register:
383 yield self.register( 359 extra = {}
384 client, None, {}, service, node, event_id, item_elt, creator=True) 360 self.onInvitationPreflight(
385 defer.returnValue(node) 361 client, "", extra, service, node, event_id, item_elt
362 )
363 await self.host.plugins['LIST_INTEREST'].registerPubsub(
364 client, NS_EVENT, service, node, event_id, True,
365 extra.pop("name", ""), extra.pop("element"), extra
366 )
367 return node
386 368
387 def _eventModify(self, service, node, id_, timestamp_update, data_update, 369 def _eventModify(self, service, node, id_, timestamp_update, data_update,
388 profile_key=C.PROF_KEY_NONE): 370 profile_key=C.PROF_KEY_NONE):
389 service = jid.JID(service) if service else None 371 service = jid.JID(service) if service else None
390 if not node: 372 if not node:
391 raise ValueError(_("missing node")) 373 raise ValueError(_("missing node"))
392 client = self.host.getClient(profile_key) 374 client = self.host.getClient(profile_key)
393 return self.eventModify( 375 return defer.ensureDeferred(
394 client, service, node, id_ or NS_EVENT, timestamp_update or None, data_update 376 self.eventModify(
395 ) 377 client, service, node, id_ or NS_EVENT, timestamp_update or None,
396 378 data_update
397 @defer.inlineCallbacks 379 )
398 def eventModify( 380 )
381
382 async def eventModify(
399 self, client, service, node, id_=NS_EVENT, timestamp_update=None, data_update=None 383 self, client, service, node, id_=NS_EVENT, timestamp_update=None, data_update=None
400 ): 384 ):
401 """Update an event 385 """Update an event
402 386
403 Similar as create instead that it update existing item instead of 387 Similar as create instead that it update existing item instead of
404 creating or replacing it. Params are the same as for [eventCreate]. 388 creating or replacing it. Params are the same as for [eventCreate].
405 """ 389 """
406 event_timestamp, event_metadata = yield self.eventGet(client, service, node, id_) 390 event_timestamp, event_metadata = await self.eventGet(client, service, node, id_)
407 new_timestamp = event_timestamp if timestamp_update is None else timestamp_update 391 new_timestamp = event_timestamp if timestamp_update is None else timestamp_update
408 new_data = event_metadata 392 new_data = event_metadata
409 if data_update: 393 if data_update:
410 for k, v in data_update.items(): 394 for k, v in data_update.items():
411 new_data[k] = v 395 new_data[k] = v
412 yield self.eventCreate(client, new_timestamp, new_data, service, node, id_) 396 await self.eventCreate(client, new_timestamp, new_data, service, node, id_)
413 397
414 def _eventsListSerialise(self, events): 398 def _eventsListSerialise(self, events):
415 for timestamp, data in events: 399 for timestamp, data in events:
416 data["date"] = str(timestamp) 400 data["date"] = str(timestamp)
417 data["creator"] = C.boolConst(data.get("creator", False)) 401 data["creator"] = C.boolConst(data.get("creator", False))
541 except KeyError: 525 except KeyError:
542 continue 526 continue
543 invitees[item["id"]] = data 527 invitees[item["id"]] = data
544 defer.returnValue(invitees) 528 defer.returnValue(invitees)
545 529
546 def _invite(self, invitee_jid, service, node, item_id, profile): 530 async def invitePreflight(
547 client = self.host.getClient(profile) 531 self,
548 service = jid.JID(service) if service else None 532 client: SatXMPPEntity,
549 node = node or None 533 invitee_jid: jid.JID,
550 item_id = item_id or None 534 service: jid.JID,
551 invitee_jid = jid.JID(invitee_jid) 535 node: str,
552 return self.invite(client, invitee_jid, service, node, item_id) 536 item_id: Optional[str] = None,
553 537 name: str = '',
554 @defer.inlineCallbacks 538 extra: Optional[dict] = None,
555 def invite(self, client, invitee_jid, service, node, item_id=NS_EVENT): 539 ) -> None:
556 """Invite an entity to the event
557
558 This will set permission to let the entity access everything needed
559 @pararm invitee_jid(jid.JID): entity to invite
560 @param service(jid.JID, None): pubsub service
561 None to use client's PEP
562 @param node(unicode): event node
563 @param item_id(unicode): event id
564 """
565 # FIXME: handle name and extra
566 name = ''
567 extra = {}
568 if self._b is None: 540 if self._b is None:
569 raise exceptions.FeatureNotFound( 541 raise exceptions.FeatureNotFound(
570 _('"XEP-0277" (blog) plugin is needed for this feature') 542 _('"XEP-0277" (blog) plugin is needed for this feature')
571 ) 543 )
572 if item_id is None: 544 if item_id is None:
573 item_id = NS_EVENT 545 item_id = extra["default_item_id"] = NS_EVENT
574 546
575 # first we authorize our invitee to see the nodes of interest 547 __, event_data = await self.eventGet(client, service, node, item_id)
576 yield self._p.setNodeAffiliations(client, service, node, {invitee_jid: "member"})
577 log.debug(_("affiliation set on event node"))
578 __, event_data = yield self.eventGet(client, service, node, item_id)
579 log.debug(_("got event data")) 548 log.debug(_("got event data"))
580 invitees_service = jid.JID(event_data["invitees_service"]) 549 invitees_service = jid.JID(event_data["invitees_service"])
581 invitees_node = event_data["invitees_node"] 550 invitees_node = event_data["invitees_node"]
582 blog_service = jid.JID(event_data["blog_service"]) 551 blog_service = jid.JID(event_data["blog_service"])
583 blog_node = event_data["blog_node"] 552 blog_node = event_data["blog_node"]
584 yield self._p.setNodeAffiliations( 553 await self._p.setNodeAffiliations(
585 client, invitees_service, invitees_node, {invitee_jid: "publisher"} 554 client, invitees_service, invitees_node, {invitee_jid: "publisher"}
586 ) 555 )
587 log.debug(_("affiliation set on invitee node")) 556 log.debug(
588 yield self._p.setNodeAffiliations( 557 f"affiliation set on invitee node (jid: {invitees_service}, "
558 f"node: {invitees_node!r})"
559 )
560 await self._p.setNodeAffiliations(
589 client, blog_service, blog_node, {invitee_jid: "member"} 561 client, blog_service, blog_node, {invitee_jid: "member"}
590 ) 562 )
591 blog_items, __ = yield self._b.mbGet(client, blog_service, blog_node, None) 563 blog_items, __ = await self._b.mbGet(client, blog_service, blog_node, None)
592 564
593 for item in blog_items: 565 for item in blog_items:
594 try: 566 try:
595 comments_service = jid.JID(item["comments_service"]) 567 comments_service = jid.JID(item["comments_service"])
596 comments_node = item["comments_node"] 568 comments_node = item["comments_node"]
599 "no comment service set for itemĀ {item_id}".format( 571 "no comment service set for itemĀ {item_id}".format(
600 item_id=item["id"] 572 item_id=item["id"]
601 ) 573 )
602 ) 574 )
603 else: 575 else:
604 yield self._p.setNodeAffiliations( 576 await self._p.setNodeAffiliations(
605 client, comments_service, comments_node, {invitee_jid: "publisher"} 577 client, comments_service, comments_node, {invitee_jid: "publisher"}
606 ) 578 )
607 log.debug(_("affiliation set on blog and comments nodes")) 579 log.debug(_("affiliation set on blog and comments nodes"))
608 580
609 # now we send the invitation 581 def _invite(self, invitee_jid, service, node, item_id, profile):
610 pubsub_invitation = self.host.plugins['INVITATION'] 582 return self.host.plugins["PUBSUB_INVITATION"]._sendPubsubInvitation(
611 pubsub_invitation.sendPubsubInvitation(client, invitee_jid, service, node, 583 invitee_jid, service, node, item_id or NS_EVENT, profile_key=profile
612 item_id, name, extra) 584 )
613 585
614 def _inviteByEmail(self, service, node, id_=NS_EVENT, email="", emails_extra=None, 586 def _inviteByEmail(self, service, node, id_=NS_EVENT, email="", emails_extra=None,
615 name="", host_name="", language="", url_template="", 587 name="", host_name="", language="", url_template="",
616 message_subject="", message_body="", 588 message_subject="", message_body="",
617 profile_key=C.PROF_KEY_NONE): 589 profile_key=C.PROF_KEY_NONE):
629 "message_subject", 601 "message_subject",
630 "message_body", 602 "message_body",
631 ): 603 ):
632 value = locals()[key] 604 value = locals()[key]
633 kwargs[key] = str(value) 605 kwargs[key] = str(value)
634 return self.inviteByEmail( 606 return defer.ensureDeferred(self.inviteByEmail(
635 client, jid.JID(service) if service else None, node, id_ or NS_EVENT, **kwargs 607 client, jid.JID(service) if service else None, node, id_ or NS_EVENT, **kwargs
636 ) 608 ))
637 609
638 @defer.inlineCallbacks 610 async def inviteByEmail(self, client, service, node, id_=NS_EVENT, **kwargs):
639 def inviteByEmail(self, client, service, node, id_=NS_EVENT, **kwargs):
640 """High level method to create an email invitation to an event 611 """High level method to create an email invitation to an event
641 612
642 @param service(unicode, None): PubSub service 613 @param service(unicode, None): PubSub service
643 @param node(unicode): PubSub node of the event 614 @param node(unicode): PubSub node of the event
644 @param id_(unicode): id_ with even data 615 @param id_(unicode): id_ with even data
654 service = service or client.jid.userhostJID() 625 service = service or client.jid.userhostJID()
655 event_uri = xmpp_uri.buildXMPPUri( 626 event_uri = xmpp_uri.buildXMPPUri(
656 "pubsub", path=service.full(), node=node, item=id_ 627 "pubsub", path=service.full(), node=node, item=id_
657 ) 628 )
658 kwargs["extra"] = {"event_uri": event_uri} 629 kwargs["extra"] = {"event_uri": event_uri}
659 invitation_data = yield self._i.create(**kwargs) 630 invitation_data = await self._i.create(**kwargs)
660 invitee_jid = invitation_data["jid"] 631 invitee_jid = invitation_data["jid"]
661 log.debug(_("invitation created")) 632 log.debug(_("invitation created"))
662 # now that we have a jid, we can send normal invitation 633 # now that we have a jid, we can send normal invitation
663 yield self.invite(client, invitee_jid, service, node, id_) 634 await self.invite(client, invitee_jid, service, node, id_)
635
636 def onInvitationPreflight(
637 self,
638 client: SatXMPPEntity,
639 name: str,
640 extra: dict,
641 service: jid.JID,
642 node: str,
643 item_id: Optional[str],
644 item_elt: domish.Element
645 ) -> None:
646 event_elt = item_elt.event
647 link_elt = event_elt.addElement("link")
648 link_elt["service"] = service.full()
649 link_elt["node"] = node
650 link_elt["item"] = item_id
651 __, event_data = self._parseEventElt(event_elt)
652 try:
653 name = event_data["name"]
654 except KeyError:
655 pass
656 else:
657 extra["name"] = name
658 if 'image' in event_data:
659 extra["thumb_url"] = event_data['image']
660 extra["element"] = event_elt
664 661
665 662
666 @implementer(iwokkel.IDisco) 663 @implementer(iwokkel.IDisco)
667 class EventsHandler(XMPPHandler): 664 class EventsHandler(XMPPHandler):
668 665