Mercurial > libervia-backend
diff sat/plugins/plugin_xep_0376.py @ 3758:b7cef1b24f83
plugins XEP-0060, XEP-0376, XEP-0465, CLI: PAM + PSS implementation:
- update psSubscriptionsGet to use serialised return value
- implement XEP-0376 Pubsub Account Management
- implement XEP-0465 Public Pubsub Subscriptions
- CLI `pubsub` commands updated accordingly, and added `--public` flags to `subscribe`,
`Subscriptions` and `node Subscriptions get`
⚠ `XEP-0465` is speculative, the XEP has been accepted by council but not published yet.
As is should be the next one, and current latest one is `XEP-0464`, `XEP-0465` has been
anticipated.
rel 365
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 13 May 2022 18:38:05 +0200 |
parents | |
children | 524856bd7b19 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat/plugins/plugin_xep_0376.py Fri May 13 18:38:05 2022 +0200 @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 + +# SàT plugin for XEP-0376 +# Copyright (C) 2009-2021 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 typing import Dict, List, Tuple, Optional, Any +from zope.interface import implementer +from twisted.words.protocols.jabber import jid +from twisted.words.protocols.jabber.xmlstream import XMPPHandler +from wokkel import disco, iwokkel, pubsub, data_form +from sat.core.i18n import _ +from sat.core.constants import Const as C +from sat.core import exceptions +from sat.core.xmpp import SatXMPPEntity +from sat.core.log import getLogger + +log = getLogger(__name__) + +PLUGIN_INFO = { + C.PI_NAME: "Pubsub Account Management", + C.PI_IMPORT_NAME: "XEP-0376", + C.PI_TYPE: C.PLUG_TYPE_XEP, + C.PI_MODES: C.PLUG_MODE_BOTH, + C.PI_PROTOCOLS: ["XEP-0376"], + C.PI_DEPENDENCIES: ["XEP-0060"], + C.PI_MAIN: "XEP_0376", + C.PI_HANDLER: "yes", + C.PI_DESCRIPTION: _("""Pubsub Account Management"""), +} + +NS_PAM = "urn:xmpp:pam:0" + + +class XEP_0376: + + def __init__(self, host): + log.info(_("Pubsub Account Management initialization")) + self.host = host + host.registerNamespace("pam", NS_PAM) + self._p = self.host.plugins["XEP-0060"] + host.trigger.add("XEP-0060_subscribe", self.subscribe) + host.trigger.add("XEP-0060_unsubscribe", self.unsubscribe) + host.trigger.add("XEP-0060_subscriptions", self.subscriptions) + + def getHandler(self, client): + return XEP_0376_Handler() + + async def profileConnected(self, client): + if not self.host.hasFeature(client, NS_PAM): + log.warning( + "Your server doesn't support Pubsub Account Management, this is used to " + "track all your subscriptions. You may ask your server administrator to " + "install it." + ) + + async def _subRequest( + self, + client: SatXMPPEntity, + service: jid.JID, + nodeIdentifier: str, + sub_jid: Optional[jid.JID], + options: Optional[dict], + subscribe: bool + ) -> None: + if sub_jid is None: + sub_jid = client.jid.userhostJID() + iq_elt = client.IQ() + pam_elt = iq_elt.addElement((NS_PAM, "pam")) + pam_elt["jid"] = service.full() + subscribe_elt = pam_elt.addElement( + (pubsub.NS_PUBSUB, "subscribe" if subscribe else "unsubscribe") + ) + subscribe_elt["node"] = nodeIdentifier + subscribe_elt["jid"] = sub_jid.full() + if options: + options_elt = pam_elt.addElement((pubsub.NS_PUBSUB, "options")) + options_elt["node"] = nodeIdentifier + options_elt["jid"] = sub_jid.full() + form = data_form.Form( + formType='submit', + formNamespace=pubsub.NS_PUBSUB_SUBSCRIBE_OPTIONS + ) + form.makeFields(options) + options_elt.addChild(form.toElement()) + + await iq_elt.send(client.server_jid.full()) + + async def subscribe( + self, + client: SatXMPPEntity, + service: jid.JID, + nodeIdentifier: str, + sub_jid: Optional[jid.JID] = None, + options: Optional[dict] = None + ) -> Tuple[bool, Optional[pubsub.Subscription]]: + if not self.host.hasFeature(client, NS_PAM) or client.is_component: + return True, None + + await self._subRequest(client, service, nodeIdentifier, sub_jid, options, True) + + # TODO: actual result is sent with <message> stanza, we have to get and use them + # to known the actual result. XEP-0376 returns an empty <iq> result, thus we don't + # know here is the subscription actually succeeded + + sub_id = None + sub = pubsub.Subscription(nodeIdentifier, sub_jid, "subscribed", options, sub_id) + return False, sub + + async def unsubscribe( + self, + client: SatXMPPEntity, + service: jid.JID, + nodeIdentifier: str, + sub_jid: Optional[jid.JID], + subscriptionIdentifier: Optional[str], + sender: Optional[jid.JID] = None, + ) -> bool: + if not self.host.hasFeature(client, NS_PAM) or client.is_component: + return True + await self._subRequest(client, service, nodeIdentifier, sub_jid, None, False) + return False + + async def subscriptions( + self, + client: SatXMPPEntity, + service: Optional[jid.JID], + node: str, + ) -> Tuple[bool, Optional[List[Dict[str, Any]]]]: + if not self.host.hasFeature(client, NS_PAM): + return True, None + if service is not None or node is not None: + # if we have service and/or node subscriptions, it's a regular XEP-0060 + # subscriptions request + return True, None + + iq_elt = client.IQ("get") + subscriptions_elt = iq_elt.addElement((NS_PAM, "subscriptions")) + result_elt = await iq_elt.send() + try: + subscriptions_elt = next(result_elt.elements(NS_PAM, "subscriptions")) + except StopIteration: + raise ValueError(f"invalid PAM response: {result_elt.toXml()}") + subs = [] + for subscription_elt in subscriptions_elt.elements(NS_PAM, "subscription"): + sub = {} + try: + for attr, key in ( + ("service", "service"), + ("node", "node"), + ("jid", "subscriber"), + ("subscription", "state") + ): + sub[key] = subscription_elt[attr] + except KeyError as e: + log.warning( + f"Invalid <subscription> element (missing {e.args[0]!r} attribute): " + f"{subscription_elt.toXml()}" + ) + continue + sub_id = subscription_elt.getAttribute("subid") + if sub_id: + sub["id"] = sub_id + subs.append(sub) + + return False, subs + + +@implementer(iwokkel.IDisco) +class XEP_0376_Handler(XMPPHandler): + + def getDiscoInfo(self, requestor, service, nodeIdentifier=""): + return [disco.DiscoFeature(NS_PAM)] + + def getDiscoItems(self, requestor, service, nodeIdentifier=""): + return []