comparison 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
comparison
equal deleted inserted replaced
3757:5bda9d2e8b35 3758:b7cef1b24f83
1 #!/usr/bin/env python3
2
3 # SàT plugin for XEP-0376
4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
5
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Affero General Public License for more details.
15
16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 from typing import Dict, List, Tuple, Optional, Any
20 from zope.interface import implementer
21 from twisted.words.protocols.jabber import jid
22 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
23 from wokkel import disco, iwokkel, pubsub, data_form
24 from sat.core.i18n import _
25 from sat.core.constants import Const as C
26 from sat.core import exceptions
27 from sat.core.xmpp import SatXMPPEntity
28 from sat.core.log import getLogger
29
30 log = getLogger(__name__)
31
32 PLUGIN_INFO = {
33 C.PI_NAME: "Pubsub Account Management",
34 C.PI_IMPORT_NAME: "XEP-0376",
35 C.PI_TYPE: C.PLUG_TYPE_XEP,
36 C.PI_MODES: C.PLUG_MODE_BOTH,
37 C.PI_PROTOCOLS: ["XEP-0376"],
38 C.PI_DEPENDENCIES: ["XEP-0060"],
39 C.PI_MAIN: "XEP_0376",
40 C.PI_HANDLER: "yes",
41 C.PI_DESCRIPTION: _("""Pubsub Account Management"""),
42 }
43
44 NS_PAM = "urn:xmpp:pam:0"
45
46
47 class XEP_0376:
48
49 def __init__(self, host):
50 log.info(_("Pubsub Account Management initialization"))
51 self.host = host
52 host.registerNamespace("pam", NS_PAM)
53 self._p = self.host.plugins["XEP-0060"]
54 host.trigger.add("XEP-0060_subscribe", self.subscribe)
55 host.trigger.add("XEP-0060_unsubscribe", self.unsubscribe)
56 host.trigger.add("XEP-0060_subscriptions", self.subscriptions)
57
58 def getHandler(self, client):
59 return XEP_0376_Handler()
60
61 async def profileConnected(self, client):
62 if not self.host.hasFeature(client, NS_PAM):
63 log.warning(
64 "Your server doesn't support Pubsub Account Management, this is used to "
65 "track all your subscriptions. You may ask your server administrator to "
66 "install it."
67 )
68
69 async def _subRequest(
70 self,
71 client: SatXMPPEntity,
72 service: jid.JID,
73 nodeIdentifier: str,
74 sub_jid: Optional[jid.JID],
75 options: Optional[dict],
76 subscribe: bool
77 ) -> None:
78 if sub_jid is None:
79 sub_jid = client.jid.userhostJID()
80 iq_elt = client.IQ()
81 pam_elt = iq_elt.addElement((NS_PAM, "pam"))
82 pam_elt["jid"] = service.full()
83 subscribe_elt = pam_elt.addElement(
84 (pubsub.NS_PUBSUB, "subscribe" if subscribe else "unsubscribe")
85 )
86 subscribe_elt["node"] = nodeIdentifier
87 subscribe_elt["jid"] = sub_jid.full()
88 if options:
89 options_elt = pam_elt.addElement((pubsub.NS_PUBSUB, "options"))
90 options_elt["node"] = nodeIdentifier
91 options_elt["jid"] = sub_jid.full()
92 form = data_form.Form(
93 formType='submit',
94 formNamespace=pubsub.NS_PUBSUB_SUBSCRIBE_OPTIONS
95 )
96 form.makeFields(options)
97 options_elt.addChild(form.toElement())
98
99 await iq_elt.send(client.server_jid.full())
100
101 async def subscribe(
102 self,
103 client: SatXMPPEntity,
104 service: jid.JID,
105 nodeIdentifier: str,
106 sub_jid: Optional[jid.JID] = None,
107 options: Optional[dict] = None
108 ) -> Tuple[bool, Optional[pubsub.Subscription]]:
109 if not self.host.hasFeature(client, NS_PAM) or client.is_component:
110 return True, None
111
112 await self._subRequest(client, service, nodeIdentifier, sub_jid, options, True)
113
114 # TODO: actual result is sent with <message> stanza, we have to get and use them
115 # to known the actual result. XEP-0376 returns an empty <iq> result, thus we don't
116 # know here is the subscription actually succeeded
117
118 sub_id = None
119 sub = pubsub.Subscription(nodeIdentifier, sub_jid, "subscribed", options, sub_id)
120 return False, sub
121
122 async def unsubscribe(
123 self,
124 client: SatXMPPEntity,
125 service: jid.JID,
126 nodeIdentifier: str,
127 sub_jid: Optional[jid.JID],
128 subscriptionIdentifier: Optional[str],
129 sender: Optional[jid.JID] = None,
130 ) -> bool:
131 if not self.host.hasFeature(client, NS_PAM) or client.is_component:
132 return True
133 await self._subRequest(client, service, nodeIdentifier, sub_jid, None, False)
134 return False
135
136 async def subscriptions(
137 self,
138 client: SatXMPPEntity,
139 service: Optional[jid.JID],
140 node: str,
141 ) -> Tuple[bool, Optional[List[Dict[str, Any]]]]:
142 if not self.host.hasFeature(client, NS_PAM):
143 return True, None
144 if service is not None or node is not None:
145 # if we have service and/or node subscriptions, it's a regular XEP-0060
146 # subscriptions request
147 return True, None
148
149 iq_elt = client.IQ("get")
150 subscriptions_elt = iq_elt.addElement((NS_PAM, "subscriptions"))
151 result_elt = await iq_elt.send()
152 try:
153 subscriptions_elt = next(result_elt.elements(NS_PAM, "subscriptions"))
154 except StopIteration:
155 raise ValueError(f"invalid PAM response: {result_elt.toXml()}")
156 subs = []
157 for subscription_elt in subscriptions_elt.elements(NS_PAM, "subscription"):
158 sub = {}
159 try:
160 for attr, key in (
161 ("service", "service"),
162 ("node", "node"),
163 ("jid", "subscriber"),
164 ("subscription", "state")
165 ):
166 sub[key] = subscription_elt[attr]
167 except KeyError as e:
168 log.warning(
169 f"Invalid <subscription> element (missing {e.args[0]!r} attribute): "
170 f"{subscription_elt.toXml()}"
171 )
172 continue
173 sub_id = subscription_elt.getAttribute("subid")
174 if sub_id:
175 sub["id"] = sub_id
176 subs.append(sub)
177
178 return False, subs
179
180
181 @implementer(iwokkel.IDisco)
182 class XEP_0376_Handler(XMPPHandler):
183
184 def getDiscoInfo(self, requestor, service, nodeIdentifier=""):
185 return [disco.DiscoFeature(NS_PAM)]
186
187 def getDiscoItems(self, requestor, service, nodeIdentifier=""):
188 return []