Mercurial > libervia-backend
view libervia/backend/plugins/plugin_xep_0471.py @ 4336:6e0918e638ee
plugin XEP-0498: "Pubsub File Sharing" implementation:
Partial implementation of XEP-0498, necessary to implement the service part in email
gateway.
rel 453
author | Goffi <goffi@goffi.org> |
---|---|
date | Tue, 03 Dec 2024 00:13:23 +0100 |
parents | 0d7bb4df2343 |
children |
line wrap: on
line source
#!/usr/bin/env python3 # Libervia plugin to handle events # Copyright (C) 2009-2022 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 random import seed from typing import Optional, Final, Dict, List, Union, Any, Optional from attr import attr import shortuuid from sqlalchemy.orm.events import event from libervia.backend.core.xmpp import SatXMPPClient from libervia.backend.core.i18n import _ from libervia.backend.core import exceptions from libervia.backend.core.constants import Const as C from libervia.backend.core.log import getLogger from libervia.backend.core.xmpp import SatXMPPEntity from libervia.backend.core.core_types import SatXMPPEntity from libervia.backend.tools import utils from libervia.backend.tools import xml_tools from libervia.backend.tools.common import uri as xmpp_uri from libervia.backend.tools.common import date_utils from libervia.backend.tools.common import data_format from twisted.internet import defer from twisted.words.protocols.jabber import jid, error from twisted.words.xish import domish from wokkel import disco, iwokkel from zope.interface import implementer from twisted.words.protocols.jabber.xmlstream import XMPPHandler from wokkel import pubsub, data_form log = getLogger(__name__) PLUGIN_INFO = { C.PI_NAME: "Events", C.PI_IMPORT_NAME: "XEP-0471", C.PI_TYPE: "XEP", C.PI_MODES: C.PLUG_MODE_BOTH, C.PI_PROTOCOLS: [], C.PI_DEPENDENCIES: [ "XEP-0060", "XEP-0080", "XEP-0447", "XEP-0470", # "INVITATION", "PUBSUB_INVITATION", # "LIST_INTEREST" ], C.PI_RECOMMENDATIONS: ["XEP-0277", "EMAIL_INVITATION"], C.PI_MAIN: "XEP_0471", C.PI_HANDLER: "yes", C.PI_DESCRIPTION: _("""Calendar Events"""), } NS_EVENT = "org.salut-a-toi.event:0" NS_EVENTS: Final = "urn:xmpp:events:0" NS_RSVP: Final = "urn:xmpp:events:rsvp:0" NS_EXTRA: Final = "urn:xmpp:events:extra:0" class XEP_0471: namespace = NS_EVENTS def __init__(self, host): log.info(_("Events plugin initialization")) self.host = host self._p = host.plugins["XEP-0060"] self._g = host.plugins["XEP-0080"] self._b = host.plugins.get("XEP-0277") self._sfs = host.plugins["XEP-0447"] self._a = host.plugins["XEP-0470"] # self._i = host.plugins.get("EMAIL_INVITATION") host.register_namespace("events", NS_EVENTS) self._a.register_attachment_handler( "rsvp", NS_EVENTS, self.rsvp_get, self.rsvp_set ) # host.plugins["PUBSUB_INVITATION"].register(NS_EVENTS, self) host.bridge.add_method( "events_get", ".plugin", in_sign="ssasss", out_sign="s", method=self._events_get, async_=True, ) host.bridge.add_method( "event_create", ".plugin", in_sign="sssss", out_sign="", method=self._event_create, async_=True, ) host.bridge.add_method( "event_modify", ".plugin", in_sign="sssss", out_sign="", method=self._event_modify, async_=True, ) host.bridge.add_method( "event_invitee_get", ".plugin", in_sign="sssasss", out_sign="s", method=self._event_invitee_get, async_=True, ) host.bridge.add_method( "event_invitee_set", ".plugin", in_sign="sssss", out_sign="", method=self._event_invitee_set, async_=True, ) host.bridge.add_method( "event_invitees_list", ".plugin", in_sign="sss", out_sign="a{sa{ss}}", method=self._event_invitees_list, async_=True, ), host.bridge.add_method( "event_invite", ".plugin", in_sign="sssss", out_sign="", method=self._invite, async_=True, ) host.bridge.add_method( "event_invite_by_email", ".plugin", in_sign="ssssassssssss", out_sign="", method=self._invite_by_email, async_=True, ) def get_handler(self, client): return EventsHandler(self) def _parse_event_elt(self, event_elt): """Helper method to parse event element @param (domish.Element): event_elt @return (tuple[int, dict[unicode, unicode]): timestamp, event_data """ try: timestamp = date_utils.date_parse(next(event_elt.elements(NS_EVENT, "date"))) except StopIteration: timestamp = -1 data = {} for key in ("name",): try: data[key] = event_elt[key] except KeyError: continue for elt_name in ("description",): try: elt = next(event_elt.elements(NS_EVENT, elt_name)) except StopIteration: continue else: data[elt_name] = str(elt) for elt_name in ("image", "background-image"): try: image_elt = next(event_elt.elements(NS_EVENT, elt_name)) data[elt_name] = image_elt["src"] except StopIteration: continue except KeyError: log.warning(_("no src found for image")) for uri_type in ("invitees", "blog"): try: elt = next(event_elt.elements(NS_EVENT, uri_type)) uri = data[uri_type + "_uri"] = elt["uri"] uri_data = xmpp_uri.parse_xmpp_uri(uri) if uri_data["type"] != "pubsub": raise ValueError except StopIteration: log.warning(_("no {uri_type} element found!").format(uri_type=uri_type)) except KeyError: log.warning(_("incomplete {uri_type} element").format(uri_type=uri_type)) except ValueError: log.warning(_("bad {uri_type} element").format(uri_type=uri_type)) else: data[uri_type + "_service"] = uri_data["path"] data[uri_type + "_node"] = uri_data["node"] for meta_elt in event_elt.elements(NS_EVENT, "meta"): key = meta_elt["name"] if key in data: log.warning( "Ignoring conflicting meta element: {xml}".format( xml=meta_elt.toXml() ) ) continue data[key] = str(meta_elt) if event_elt.link: link_elt = event_elt.link data["service"] = link_elt["service"] data["node"] = link_elt["node"] data["item"] = link_elt["item"] if event_elt.getAttribute("creator") == "true": data["creator"] = True return timestamp, data def event_elt_2_event_data(self, event_elt: domish.Element) -> Dict[str, Any]: """Convert <event/> element to event data @param event_elt: <event/> element parent <item/> element can also be used @raise exceptions.NotFound: can't find event payload @raise ValueError: something is missing or badly formed """ if event_elt.name == "item": try: event_elt = next(event_elt.elements(NS_EVENTS, "event")) except StopIteration: raise exceptions.NotFound("<event/> payload is missing") event_data: Dict[str, Any] = {} # id parent_elt = event_elt.parent if parent_elt is not None and parent_elt.hasAttribute("id"): event_data["id"] = parent_elt["id"] # name name_data: Dict[str, str] = {} event_data["name"] = name_data for name_elt in event_elt.elements(NS_EVENTS, "name"): lang = name_elt.getAttribute("xml:lang", "") if lang in name_data: raise ValueError("<name/> elements don't have distinct xml:lang") name_data[lang] = str(name_elt) if not name_data: raise exceptions.NotFound("<name/> element is missing") # start try: start_elt = next(event_elt.elements(NS_EVENTS, "start")) except StopIteration: raise exceptions.NotFound("<start/> element is missing") event_data["start"] = utils.parse_xmpp_date(str(start_elt)) # end try: end_elt = next(event_elt.elements(NS_EVENTS, "end")) except StopIteration: raise exceptions.NotFound("<end/> element is missing") event_data["end"] = utils.parse_xmpp_date(str(end_elt)) # head-picture head_pic_elt = next(event_elt.elements(NS_EVENTS, "head-picture"), None) if head_pic_elt is not None: event_data["head-picture"] = self._sfs.parse_file_sharing_elt(head_pic_elt) # description seen_desc = set() for description_elt in event_elt.elements(NS_EVENTS, "description"): lang = description_elt.getAttribute("xml:lang", "") desc_type = description_elt.getAttribute("type", "text") lang_type = (lang, desc_type) if lang_type in seen_desc: raise ValueError( "<description/> elements don't have distinct xml:lang/type" ) seen_desc.add(lang_type) descriptions = event_data.setdefault("descriptions", []) description_data = {"description": str(description_elt)} if lang: description_data["language"] = lang if desc_type: description_data["type"] = desc_type descriptions.append(description_data) # categories for category_elt in event_elt.elements(NS_EVENTS, "category"): try: category_data = {"term": category_elt["term"]} except KeyError: log.warning( "<category/> element is missing mandatory term: " f"{category_elt.toXml()}" ) continue wd = category_elt.getAttribute("wd") if wd: category_data["wikidata_id"] = wd lang = category_elt.getAttribute("xml:lang") if lang: category_data["language"] = lang event_data.setdefault("categories", []).append(category_data) # locations seen_location_ids = set() for location_elt in event_elt.elements(NS_EVENTS, "location"): location_id = location_elt.getAttribute("id", "") if location_id in seen_location_ids: raise ValueError("<location/> elements don't have distinct IDs") seen_location_ids.add(location_id) location_data = self._g.parse_geoloc_elt(location_elt) if location_id: location_data["id"] = location_id lang = location_elt.getAttribute("xml:lang", "") if lang: location_data["language"] = lang event_data.setdefault("locations", []).append(location_data) # RSVPs seen_rsvp_lang = set() for rsvp_elt in event_elt.elements(NS_EVENTS, "rsvp"): rsvp_lang = rsvp_elt.getAttribute("xml:lang", "") if rsvp_lang in seen_rsvp_lang: raise ValueError("<rsvp/> elements don't have distinct xml:lang") seen_rsvp_lang.add(rsvp_lang) rsvp_form = data_form.findForm(rsvp_elt, NS_RSVP) if rsvp_form is None: log.warning(f"RSVP form is missing: {rsvp_elt.toXml()}") continue rsvp_data = xml_tools.data_form_2_data_dict(rsvp_form) if rsvp_lang: rsvp_data["language"] = rsvp_lang event_data.setdefault("rsvp", []).append(rsvp_data) # linked pubsub nodes for name in ("invitees", "comments", "blog", "schedule"): elt = next(event_elt.elements(NS_EVENTS, name), None) if elt is not None: try: event_data[name] = {"service": elt["jid"], "node": elt["node"]} except KeyError: log.warning(f"invalid {name} element: {elt.toXml()}") # attachments attachments_elt = next(event_elt.elements(NS_EVENTS, "attachments"), None) if attachments_elt: attachments = event_data["attachments"] = [] for file_sharing_elt in attachments_elt.elements( self._sfs.namespace, "file-sharing" ): try: file_sharing_data = self._sfs.parse_file_sharing_elt(file_sharing_elt) except Exception as e: log.warning(f"invalid attachment: {e}\n{file_sharing_elt.toXml()}") continue attachments.append(file_sharing_data) # extra extra_elt = next(event_elt.elements(NS_EVENTS, "extra"), None) if extra_elt is not None: extra_form = data_form.findForm(extra_elt, NS_EXTRA) if extra_form is None: log.warning(f"extra form is missing: {extra_elt.toXml()}") else: extra_data = event_data["extra"] = {} for name, value in extra_form.items(): if name.startswith("accessibility:"): extra_data.setdefault("accessibility", {})[name[14:]] = value elif name == "accessibility": log.warning( 'ignoring "accessibility" key which is not standard: ' f"{extra_form.toElement().toXml()}" ) else: extra_data[name] = value # external external_elt = next(event_elt.elements(NS_EVENTS, "external"), None) if external_elt: try: event_data["external"] = { "jid": external_elt["jid"], "node": external_elt["node"], "item": external_elt["item"], } except KeyError: log.warning(f"invalid <external/> element: {external_elt.toXml()}") return event_data def _events_get( self, service: str, node: str, event_ids: List[str], extra: str, profile_key: str ): client = self.host.get_client(profile_key) d = defer.ensureDeferred( self.events_get( client, jid.JID(service) if service else None, node if node else NS_EVENTS, event_ids, data_format.deserialise(extra), ) ) d.addCallback(data_format.serialise) return d async def events_get( self, client: SatXMPPEntity, service: Optional[jid.JID], node: str = NS_EVENTS, events_ids: Optional[List[str]] = None, extra: Optional[dict] = None, ) -> List[Dict[str, Any]]: """Retrieve event data @param service: pubsub service @param node: pubsub node @param event_id: pubsub item ID @return: event data: """ if service is None: service = client.jid.userhostJID() items, __ = await self._p.get_items( client, service, node, item_ids=events_ids, extra=extra ) events = [] for item in items: try: events.append(self.event_elt_2_event_data((item))) except (ValueError, exceptions.NotFound): log.warning(f"Can't parse event for item {item['id']}: {item.toXml()}") return events def _event_create( self, data_s: str, service: str, node: str, event_id: str = "", profile_key: str = C.PROF_KEY_NONE, ): client = self.host.get_client(profile_key) return defer.ensureDeferred( self.event_create( client, data_format.deserialise(data_s), jid.JID(service) if service else None, node or None, event_id or None, ) ) def event_data_2_event_elt(self, event_data: Dict[str, Any]) -> domish.Element: """Convert Event Data to corresponding Element @param event_data: data of the event with keys as follow: name (dict) map of language to name empty string can be used as key if no language is specified this key is mandatory start (int|float) starting time of the event this key is mandatory end (int|float) ending time of the event this key is mandatory head-picture(dict) file sharing data for the main picture to use to represent the event description(list[dict]) list of descriptions. If there are several descriptions, they must have distinct (language, type). Description data is dict which following keys: description(str) the description itself, either in plain text or xhtml this key is mandatory language(str) ISO-639 language code type(str) type of the description, either "text" (default) or "xhtml" categories(list[dict]) each category is a dict with following keys: term(str) human readable short text of the category this key is mandatory wikidata_id(str) Entity ID from WikiData language(str) ISO-639 language code locations(list[dict]) list of location dict as used in plugin XEP-0080 [get_geoloc_elt]. If several locations are used, they must have distinct IDs rsvp(list[dict]) RSVP data. The dict is a data dict as used in sat.tools.xml_tools.data_dict_2_data_form with some extra keys. The "attending" key is automatically added if it's not already present, except if the "no_default" key is present. Thus, an empty dict can be used to use default RSVP. If several dict are present in the list, they must have different "lang" keys. Following extra key can be used: language(str) ISO-639 code for language used in the form no_default(bool) if True, the "attending" field won't be automatically added invitees(dict) link to pubsub node holding invitees list. Following keys are mandatory: service(str) pubsub service where the node is node (str) pubsub node to use comments(dict) link to pubsub node holding XEP-0277 comments on the event itself. Following keys are mandatory: service(str) pubsub service where the node is node (str) pubsub node to use blog(dict) link to pubsub node holding a blog about the event. Following keys are mandatory: service(str) pubsub service where the node is node (str) pubsub node to use schedule(dict) link to pubsub node holding an events node describing the schedule of this event. Following keys are mandatory: service(str) pubsub service where the node is node (str) pubsub node to use attachments[list[dict]] list of file sharing data about all kind of attachments of interest for the event. extra(dict) extra information about the event. Keys can be: website(str) main website about the event status(str) status of the event. Can be one of "confirmed", "tentative" or "cancelled" languages(list[str]) ISO-639 codes for languages which will be mainly spoken at the event accessibility(dict) accessibility informations. Keys can be: wheelchair tell if the event is accessible to wheelchair. Value can be "full", "partial" or "no" external(dict): if present, this event is a link to an external one. Keys (all mandatory) are: jid: pubsub service node: pubsub node item: event id @return: Event element @raise ValueError: some expected data were missing or incorrect """ event_elt = domish.Element((NS_EVENTS, "event")) try: for lang, name in event_data["name"].items(): name_elt = event_elt.addElement("name", content=name) if lang: name_elt["xml:lang"] = lang except (KeyError, TypeError): raise ValueError('"name" field is not a dict mapping language to event name') try: event_elt.addElement("start", content=utils.xmpp_date(event_data["start"])) event_elt.addElement("end", content=utils.xmpp_date(event_data["end"])) except (KeyError, TypeError, ValueError): raise ValueError('"start" and "end" fields are mandatory') if "head-picture" in event_data: head_pic_data = event_data["head-picture"] head_picture_elt = event_elt.addElement("head-picture") head_picture_elt.addChild(self._sfs.get_file_sharing_elt(**head_pic_data)) seen_desc = set() if "descriptions" in event_data: for desc_data in event_data["descriptions"]: desc_type = desc_data.get("type", "text") lang = desc_data.get("language") or "" lang_type = (lang, desc_type) if lang_type in seen_desc: raise ValueError( '"xml:lang" and "type" is not unique among descriptions: ' f"{desc_data}" ) seen_desc.add(lang_type) try: description = desc_data["description"] except KeyError: log.warning(f"description is missing in {desc_data!r}") continue if desc_type == "text": description_elt = event_elt.addElement( "description", content=description ) elif desc_type == "xhtml": description_elt = event_elt.addElement("description") div_elt = xml_tools.parse(description, namespace=C.NS_XHTML) description_elt.addChild(div_elt) else: log.warning(f"unknown description type {desc_type!r}") continue if lang: description_elt["xml:lang"] = lang for category_data in event_data.get("categories", []): try: category_term = category_data["term"] except KeyError: log.warning(f'"term" is missing categories data: {category_data}') continue category_elt = event_elt.addElement("category") category_elt["term"] = category_term category_wd = category_data.get("wikidata_id") if category_wd: category_elt["wd"] = category_wd category_lang = category_data.get("language") if category_lang: category_elt["xml:lang"] = category_lang seen_location_ids = set() for location_data in event_data.get("locations", []): location_id = location_data.get("id", "") if location_id in seen_location_ids: raise ValueError("locations must have distinct IDs") seen_location_ids.add(location_id) location_elt = event_elt.addElement("location") location_elt.addChild(self._g.get_geoloc_elt(location_data)) if location_id: location_elt["id"] = location_id rsvp_data_list: Optional[List[dict]] = event_data.get("rsvp") if rsvp_data_list is not None: seen_lang = set() for rsvp_data in rsvp_data_list: if not rsvp_data: # we use a minimum data if an empty dict is received. It will be later # filled with defaut "attending" field. rsvp_data = {"fields": []} rsvp_elt = event_elt.addElement("rsvp") lang = rsvp_data.get("language", "") if lang in seen_lang: raise ValueError( "If several RSVP are specified, they must have distinct " f"languages. {lang!r} language has been used several times." ) seen_lang.add(lang) if lang: rsvp_elt["xml:lang"] = lang if not rsvp_data.get("no_default", False): try: next(f for f in rsvp_data["fields"] if f["name"] == "attending") except StopIteration: rsvp_data["fields"].append( { "type": "list-single", "name": "attending", "label": "Attending", "options": [ {"label": "maybe", "value": "maybe"}, {"label": "yes", "value": "yes"}, {"label": "no", "value": "no"}, ], "required": True, } ) rsvp_data["namespace"] = NS_RSVP rsvp_form = xml_tools.data_dict_2_data_form(rsvp_data) rsvp_elt.addChild(rsvp_form.toElement()) for node_type in ("invitees", "comments", "blog", "schedule"): node_data = event_data.get(node_type) if not node_data: continue try: service, node = node_data["service"], node_data["node"] except KeyError: log.warning(f"invalid node data for {node_type}: {node_data}") else: pub_node_elt = event_elt.addElement(node_type) pub_node_elt["jid"] = service pub_node_elt["node"] = node attachments = event_data.get("attachments") if attachments: attachments_elt = event_elt.addElement("attachments") for attachment_data in attachments: attachments_elt.addChild( self._sfs.get_file_sharing_elt(**attachment_data) ) extra = event_data.get("extra") if extra: extra_form = data_form.Form("result", formNamespace=NS_EXTRA) for node_type in ("website", "status"): if node_type in extra: extra_form.addField( data_form.Field(var=node_type, value=extra[node_type]) ) if "languages" in extra: extra_form.addField( data_form.Field( "list-multi", var="languages", values=extra["languages"] ) ) for node_type, value in extra.get("accessibility", {}).items(): extra_form.addField( data_form.Field(var=f"accessibility:{node_type}", value=value) ) extra_elt = event_elt.addElement("extra") extra_elt.addChild(extra_form.toElement()) if "external" in event_data: external_data = event_data["external"] external_elt = event_elt.addElement("external") for node_type in ("jid", "node", "item"): try: value = external_data[node_type] except KeyError: raise ValueError(f"Invalid external data: {external_data}") external_elt[node_type] = value return event_elt async def event_create( self, client: SatXMPPEntity, event_data: Dict[str, Any], service: Optional[jid.JID] = None, node: Optional[str] = None, event_id: Optional[str] = None, ) -> None: """Create or replace an event @param event_data: data of the event (cf. [event_data_2_event_elt]) @param node: PubSub node of the event None to use default node (default namespace for personal agenda) @param service: PubSub service None to use profile's PEP @param event_id: ID of the item to create. """ if not service: service = client.jid.userhostJID() if not node: node = NS_EVENTS if event_id is None: event_id = shortuuid.uuid() event_elt = self.event_data_2_event_elt(event_data) item_elt = pubsub.Item(id=event_id, payload=event_elt) options = {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_WHITELIST} await self._p.create_if_new_node( client, service, nodeIdentifier=node, options=options ) await self._p.publish(client, service, node, items=[item_elt]) if event_data.get("rsvp"): await self._a.create_attachments_node(client, service, node, event_id) def _event_modify( self, data_s: str, event_id: str, service: str, node: str, profile_key: str = C.PROF_KEY_NONE, ) -> None: client = self.host.get_client(profile_key) defer.ensureDeferred( self.event_modify( client, data_format.deserialise(data_s), event_id, jid.JID(service) if service else None, node or None, ) ) async def event_modify( self, client: SatXMPPEntity, event_data: Dict[str, Any], event_id: str, service: Optional[jid.JID] = None, node: Optional[str] = None, ) -> None: """Update an event Similar as create instead that it update existing item instead of creating or replacing it. Params are the same as for [event_create]. """ if not service: service = client.jid.userhostJID() if not node: node = NS_EVENTS old_event = (await self.events_get(client, service, node, [event_id]))[0] old_event.update(event_data) event_data = old_event await self.event_create(client, event_data, service, node, event_id) def rsvp_get( self, client: SatXMPPEntity, attachments_elt: domish.Element, data: Dict[str, Any], ) -> None: """Get RSVP answers from attachments""" try: rsvp_elt = next(attachments_elt.elements(NS_EVENTS, "rsvp")) except StopIteration: pass else: rsvp_form = data_form.findForm(rsvp_elt, NS_RSVP) if rsvp_form is not None: data["rsvp"] = rsvp_data = dict(rsvp_form) self._a.set_timestamp(rsvp_elt, rsvp_data) def rsvp_set( self, client: SatXMPPEntity, data: Dict[str, Any], former_elt: Optional[domish.Element], ) -> Optional[domish.Element]: """update the <reaction> attachment""" rsvp_data = data["extra"].get("rsvp") if rsvp_data is None: return former_elt elif rsvp_data: rsvp_elt = domish.Element( (NS_EVENTS, "rsvp"), attribs={"timestamp": utils.xmpp_date()} ) rsvp_form = data_form.Form("submit", formNamespace=NS_RSVP) rsvp_form.makeFields(rsvp_data) rsvp_elt.addChild(rsvp_form.toElement()) return rsvp_elt else: return None def _event_invitee_get( self, service: str, node: str, item: str, invitees: List[str], extra: str, profile_key: str, ) -> defer.Deferred: client = self.host.get_client(profile_key) if invitees: invitees_jid = [jid.JID(i) for i in invitees] else: invitees_jid = None d = defer.ensureDeferred( self.event_invitee_get( client, jid.JID(service) if service else None, node or None, item, invitees_jid, data_format.deserialise(extra), ) ) d.addCallback(lambda ret: data_format.serialise(ret)) return d async def event_invitee_get( self, client: SatXMPPEntity, service: Optional[jid.JID], node: Optional[str], item: str, invitees: Optional[List[jid.JID]] = None, extra: Optional[Dict[str, Any]] = None, ) -> Dict[str, Dict[str, Any]]: """Retrieve attendance from event node @param service: PubSub service @param node: PubSub node of the event @param item: PubSub item of the event @param invitees: if set, only retrieve RSVPs from those guests @param extra: extra data used to retrieve items as for [get_attachments] @return: mapping of invitee bare JID to their RSVP an empty dict is returned if nothing has been answered yed """ if service is None: service = client.jid.userhostJID() if node is None: node = NS_EVENTS attachments, metadata = await self._a.get_attachments( client, service, node, item, invitees, extra ) ret = {} for attachment in attachments: try: rsvp = attachment["rsvp"] except KeyError: continue ret[attachment["from"]] = rsvp return ret def _event_invitee_set( self, service: str, node: str, item: str, rsvp_s: str, profile_key: str ): client = self.host.get_client(profile_key) return defer.ensureDeferred( self.event_invitee_set( client, jid.JID(service) if service else None, node or None, item, data_format.deserialise(rsvp_s), ) ) async def event_invitee_set( self, client: SatXMPPEntity, service: Optional[jid.JID], node: Optional[str], item: str, rsvp: Dict[str, Any], ) -> None: """Set or update attendance data in event node @param service: PubSub service @param node: PubSub node of the event @param item: PubSub item of the event @param rsvp: RSVP data (values to submit to the form) """ if service is None: service = client.jid.userhostJID() if node is None: node = NS_EVENTS await self._a.set_attachements( client, { "service": service.full(), "node": node, "id": item, "extra": {"rsvp": rsvp}, }, ) def _event_invitees_list(self, service, node, profile_key): service = jid.JID(service) if service else None node = node if node else NS_EVENT client = self.host.get_client(profile_key) return defer.ensureDeferred(self.event_invitees_list(client, service, node)) async def event_invitees_list(self, client, service, node): """Retrieve attendance from event node @param service(unicode, None): PubSub service @param node(unicode): PubSub node of the event @return (dict): a dict with current attendance status, an empty dict is returned if nothing has been answered yed """ items, metadata = await self._p.get_items(client, service, node) invitees = {} for item in items: try: event_elt = next(item.elements(NS_EVENT, "invitee")) except StopIteration: # no item found, event data are not set yet log.warning( _( "no data found for {item_id} (service: {service}, node: {node})".format( item_id=item["id"], service=service, node=node ) ) ) else: data = {} for key in ("attend", "guests"): try: data[key] = event_elt[key] except KeyError: continue invitees[item["id"]] = data return invitees async def invite_preflight( self, client: SatXMPPEntity, invitee_jid: jid.JID, service: jid.JID, node: str, item_id: Optional[str] = None, name: str = "", extra: Optional[dict] = None, ) -> None: if self._b is None: raise exceptions.FeatureNotFound( _('"XEP-0277" (blog) plugin is needed for this feature') ) if item_id is None: item_id = extra["default_item_id"] = NS_EVENT __, event_data = await self.events_get(client, service, node, item_id) log.debug(_("got event data")) invitees_service = jid.JID(event_data["invitees_service"]) invitees_node = event_data["invitees_node"] blog_service = jid.JID(event_data["blog_service"]) blog_node = event_data["blog_node"] await self._p.set_node_affiliations( client, invitees_service, invitees_node, {invitee_jid: "publisher"} ) log.debug( f"affiliation set on invitee node (jid: {invitees_service}, " f"node: {invitees_node!r})" ) await self._p.set_node_affiliations( client, blog_service, blog_node, {invitee_jid: "member"} ) blog_items, __ = await self._b.mb_get(client, blog_service, blog_node, None) for item in blog_items: try: comments_service = jid.JID(item["comments_service"]) comments_node = item["comments_node"] except KeyError: log.debug( "no comment service set for item {item_id}".format(item_id=item["id"]) ) else: await self._p.set_node_affiliations( client, comments_service, comments_node, {invitee_jid: "publisher"} ) log.debug(_("affiliation set on blog and comments nodes")) def _invite(self, invitee_jid, service, node, item_id, profile): return self.host.plugins["PUBSUB_INVITATION"]._send_pubsub_invitation( invitee_jid, service, node, item_id or NS_EVENT, profile_key=profile ) def _invite_by_email( self, service, node, id_=NS_EVENT, email="", emails_extra=None, name="", host_name="", language="", url_template="", message_subject="", message_body="", profile_key=C.PROF_KEY_NONE, ): client = self.host.get_client(profile_key) kwargs = { "profile": client.profile, "emails_extra": [str(e) for e in emails_extra], } for key in ( "email", "name", "host_name", "language", "url_template", "message_subject", "message_body", ): value = locals()[key] kwargs[key] = str(value) return defer.ensureDeferred( self.invite_by_email( client, jid.JID(service) if service else None, node, id_ or NS_EVENT, **kwargs, ) ) async def invite_by_email(self, client, service, node, id_=NS_EVENT, **kwargs): """High level method to create an email invitation to an event @param service(unicode, None): PubSub service @param node(unicode): PubSub node of the event @param id_(unicode): id_ with even data """ if self._i is None: raise exceptions.FeatureNotFound( _('"Invitations" plugin is needed for this feature') ) if self._b is None: raise exceptions.FeatureNotFound( _('"XEP-0277" (blog) plugin is needed for this feature') ) service = service or client.jid.userhostJID() event_uri = xmpp_uri.build_xmpp_uri( "pubsub", path=service.full(), node=node, item=id_ ) kwargs["extra"] = {"event_uri": event_uri} invitation_data = await self._i.create(**kwargs) invitee_jid = invitation_data["jid"] log.debug(_("invitation created")) # now that we have a jid, we can send normal invitation await self.invite(client, invitee_jid, service, node, id_) def on_invitation_preflight( self, client: SatXMPPEntity, name: str, extra: dict, service: jid.JID, node: str, item_id: Optional[str], item_elt: domish.Element, ) -> None: event_elt = item_elt.event link_elt = event_elt.addElement("link") link_elt["service"] = service.full() link_elt["node"] = node link_elt["item"] = item_id __, event_data = self._parse_event_elt(event_elt) try: name = event_data["name"] except KeyError: pass else: extra["name"] = name if "image" in event_data: extra["thumb_url"] = event_data["image"] extra["element"] = event_elt @implementer(iwokkel.IDisco) class EventsHandler(XMPPHandler): def __init__(self, plugin_parent): self.plugin_parent = plugin_parent def getDiscoInfo(self, requestor, target, nodeIdentifier=""): return [ disco.DiscoFeature(NS_EVENTS), ] def getDiscoItems(self, requestor, target, nodeIdentifier=""): return []