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 []