libervia-backend/libervia/backend/plugins/plugin_xep_0471.py
2026-03-31 12:57:17 +02:00

1182 lines
43 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# Libervia plugin to handle events
# Copyright (C) 2009-2026 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 tx_xmpp import disco, itx_xmpp
from zope.interface import implementer
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
from tx_xmpp 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_BASE: Final = "urn:xmpp:events"
NS_EVENTS: Final = f"{NS_EVENTS_BASE}:0"
NS_RSVP: Final = f"{NS_EVENTS_BASE}:rsvp:0"
NS_EXTRA: Final = f"{NS_EVENTS_BASE}: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(itx_xmpp.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 []