Mercurial > libervia-backend
comparison sat/plugins/plugin_xep_0465.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 | 56e5b18f4d06 |
comparison
equal
deleted
inserted
replaced
3757:5bda9d2e8b35 | 3758:b7cef1b24f83 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # Libervia plugin for XEP-0465 | |
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 Optional, List, Dict, Union | |
20 | |
21 from twisted.words.protocols.jabber.xmlstream import XMPPHandler | |
22 from twisted.words.protocols.jabber import jid | |
23 from twisted.words.xish import domish | |
24 from zope.interface import implementer | |
25 from wokkel import disco, iwokkel | |
26 | |
27 from sat.core.constants import Const as C | |
28 from sat.core.i18n import _ | |
29 from sat.core.log import getLogger | |
30 from sat.core import exceptions | |
31 from sat.core.core_types import SatXMPPEntity | |
32 from sat.tools import utils | |
33 from sat.tools.common import data_format | |
34 | |
35 log = getLogger(__name__) | |
36 | |
37 PLUGIN_INFO = { | |
38 C.PI_NAME: "Pubsub Public Subscriptions", | |
39 C.PI_IMPORT_NAME: "XEP-0465", | |
40 C.PI_TYPE: C.PLUG_TYPE_XEP, | |
41 C.PI_MODES: C.PLUG_MODE_BOTH, | |
42 C.PI_PROTOCOLS: ["XEP-0465"], | |
43 C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0376"], | |
44 C.PI_MAIN: "XEP_0465", | |
45 C.PI_HANDLER: "yes", | |
46 C.PI_DESCRIPTION: _("""Pubsub Public Subscriptions implementation"""), | |
47 } | |
48 | |
49 NS_PPS = "urn:xmpp:pps:0" | |
50 NS_PPS_SUBSCRIPTIONS = "urn:xmpp:pps:subscriptions:0" | |
51 NS_PPS_SUBSCRIBERS = "urn:xmpp:pps:subscribers:0" | |
52 SUBSCRIBERS_NODE_PREFIX = f"{NS_PPS_SUBSCRIBERS}/" | |
53 | |
54 | |
55 class XEP_0465: | |
56 | |
57 def __init__(self, host): | |
58 log.info(_("Pubsub Public Subscriptions initialization")) | |
59 host.registerNamespace("pps", NS_PPS) | |
60 self.host = host | |
61 host.bridge.addMethod( | |
62 "psPublicSubscriptionsGet", | |
63 ".plugin", | |
64 in_sign="sss", | |
65 out_sign="s", | |
66 method=self._subscriptions, | |
67 async_=True, | |
68 ) | |
69 host.bridge.addMethod( | |
70 "psPublicSubscriptionsGet", | |
71 ".plugin", | |
72 in_sign="sss", | |
73 out_sign="s", | |
74 method=self._subscriptions, | |
75 async_=True, | |
76 ) | |
77 host.bridge.addMethod( | |
78 "psPublicNodeSubscriptionsGet", | |
79 ".plugin", | |
80 in_sign="sss", | |
81 out_sign="a{ss}", | |
82 method=self._getPublicNodeSubscriptions, | |
83 async_=True, | |
84 ) | |
85 | |
86 def getHandler(self, client): | |
87 return XEP_0465_Handler() | |
88 | |
89 @property | |
90 def subscriptions_node(self) -> str: | |
91 return NS_PPS_SUBSCRIPTIONS | |
92 | |
93 @property | |
94 def subscribers_node_prefix(self) -> str: | |
95 return SUBSCRIBERS_NODE_PREFIX | |
96 | |
97 def buildSubscriptionElt(self, node: str, service: jid.JID) -> domish.Element: | |
98 """Generate a <subscriptions> element | |
99 | |
100 This is the element that a service returns on public subscriptions request | |
101 """ | |
102 subscription_elt = domish.Element((NS_PPS, "subscription")) | |
103 subscription_elt["node"] = node | |
104 subscription_elt["service"] = service.full() | |
105 return subscription_elt | |
106 | |
107 def buildSubscriberElt(self, subscriber: jid.JID) -> domish.Element: | |
108 """Generate a <subscriber> element | |
109 | |
110 This is the element that a service returns on node public subscriptions request | |
111 """ | |
112 subscriber_elt = domish.Element((NS_PPS, "subscriber")) | |
113 subscriber_elt["jid"] = subscriber.full() | |
114 return subscriber_elt | |
115 | |
116 @utils.ensure_deferred | |
117 async def _subscriptions( | |
118 self, | |
119 service="", | |
120 nodeIdentifier="", | |
121 profile_key=C.PROF_KEY_NONE | |
122 ) -> str: | |
123 client = self.host.getClient(profile_key) | |
124 service = None if not service else jid.JID(service) | |
125 subs = await self.subscriptions(client, service, nodeIdentifier or None) | |
126 return data_format.serialise(subs) | |
127 | |
128 async def subscriptions( | |
129 self, | |
130 client: SatXMPPEntity, | |
131 service: Optional[jid.JID] = None, | |
132 node: Optional[str] = None | |
133 ) -> List[Dict[str, Union[str, bool]]]: | |
134 """Retrieve public subscriptions from a service | |
135 | |
136 @param service(jid.JID): PubSub service | |
137 @param nodeIdentifier(unicode, None): node to filter | |
138 None to get all subscriptions | |
139 """ | |
140 if service is None: | |
141 service = client.jid.userhostJID() | |
142 items, __ = await self.host.plugins["XEP-0060"].getItems( | |
143 client, service, NS_PPS_SUBSCRIPTIONS | |
144 ) | |
145 ret = [] | |
146 for item in items: | |
147 try: | |
148 subscription_elt = next(item.elements(NS_PPS, "subscription")) | |
149 except StopIteration: | |
150 log.warning(f"no <subscription> element found: {item.toXml()}") | |
151 continue | |
152 | |
153 try: | |
154 sub_dict = { | |
155 "service": subscription_elt["service"], | |
156 "node": subscription_elt["node"], | |
157 "subscriber": service.full(), | |
158 "state": subscription_elt.getAttribute("subscription", "subscribed"), | |
159 } | |
160 except KeyError: | |
161 log.warning( | |
162 f"invalid <subscription> element: {subscription_elt.toXml()}" | |
163 ) | |
164 continue | |
165 if node is not None and sub_dict["node"] != node: | |
166 # if not is specified, we filter out any other node | |
167 # FIXME: should node filtering be done by server? | |
168 continue | |
169 ret.append(sub_dict) | |
170 return ret | |
171 | |
172 @utils.ensure_deferred | |
173 async def _getPublicNodeSubscriptions( | |
174 self, | |
175 service: str, | |
176 node: str, | |
177 profile_key: str | |
178 ) -> Dict[str, str]: | |
179 client = self.host.getClient(profile_key) | |
180 subs = await self.getPublicNodeSubscriptions( | |
181 client, jid.JID(service) if service else None, node | |
182 ) | |
183 return {j.full(): a for j, a in subs.items()} | |
184 | |
185 def getPublicSubscribersNode(self, node: str) -> str: | |
186 """Return prefixed node to retrieve public subscribers""" | |
187 return f"{NS_PPS_SUBSCRIBERS}/{node}" | |
188 | |
189 async def getPublicNodeSubscriptions( | |
190 self, | |
191 client: SatXMPPEntity, | |
192 service: Optional[jid.JID], | |
193 nodeIdentifier: str | |
194 ) -> Dict[jid.JID, str]: | |
195 """Retrieve public subscriptions to a node | |
196 | |
197 @param nodeIdentifier(unicode): node to get subscriptions from | |
198 """ | |
199 if not nodeIdentifier: | |
200 raise exceptions.DataError("node identifier can't be empty") | |
201 | |
202 if service is None: | |
203 service = client.jid.userhostJID() | |
204 | |
205 subscribers_node = self.getPublicSubscribersNode(nodeIdentifier) | |
206 | |
207 items, __ = await self.host.plugins["XEP-0060"].getItems( | |
208 client, service, subscribers_node | |
209 ) | |
210 ret = {} | |
211 for item in items: | |
212 try: | |
213 subscriber_elt = next(item.elements(NS_PPS, "subscriber")) | |
214 except StopIteration: | |
215 log.warning(f"no <subscriber> element found: {item.toXml()}") | |
216 continue | |
217 | |
218 try: | |
219 ret[jid.JID(subscriber_elt["jid"])] = "subscribed" | |
220 except (KeyError, RuntimeError): | |
221 log.warning( | |
222 f"invalid <subscriber> element: {subscriber_elt.toXml()}" | |
223 ) | |
224 continue | |
225 return ret | |
226 | |
227 def setPublicOpt(self, options: Optional[dict] = None) -> dict: | |
228 """Set option to make a subscription public | |
229 | |
230 @param options: dict where the option must be set | |
231 if None, a new dict will be created | |
232 | |
233 @return: the options dict | |
234 """ | |
235 if options is None: | |
236 options = {} | |
237 options[f'{{{NS_PPS}}}public'] = True | |
238 return options | |
239 | |
240 | |
241 @implementer(iwokkel.IDisco) | |
242 class XEP_0465_Handler(XMPPHandler): | |
243 | |
244 def getDiscoInfo(self, requestor, service, nodeIdentifier=""): | |
245 return [disco.DiscoFeature(NS_PPS)] | |
246 | |
247 def getDiscoItems(self, requestor, service, nodeIdentifier=""): | |
248 return [] |