Mercurial > libervia-backend
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 |