comparison sat/plugins/plugin_exp_invitation.py @ 3462:12dc234f698c

plugin invitation: pubsub invitations: - new Pubsub invitation plugin, to have a generic way to manage invitation on Pubsub based features - `invitePreflight` and `onInvitationPreflight` method can be implemented to customise invitation for a namespace - refactored events invitations to use the new plugin - a Pubsub invitation can now be for a whole node instead of a specific item - if invitation is for a node, a namespace can be specified to indicate what this node is about. It is then added in `extra` data - an element (domish.Element) can be added in `extra` data, it will then be added in the invitation - some code modernisation
author Goffi <goffi@goffi.org>
date Fri, 19 Feb 2021 15:50:22 +0100
parents 000b6722bd35
children be6d91572633
comparison
equal deleted inserted replaced
3461:02a8d227d5bb 3462:12dc234f698c
14 # GNU Affero General Public License for more details. 14 # GNU Affero General Public License for more details.
15 15
16 # You should have received a copy of the GNU Affero General Public License 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/>. 17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 18
19 from typing import Optional
20 from zope.interface import implementer
21 from twisted.internet import defer
22 from twisted.words.protocols.jabber import jid
23 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
24 from wokkel import disco, iwokkel
19 from sat.core.i18n import _ 25 from sat.core.i18n import _
20 from sat.core import exceptions 26 from sat.core import exceptions
21 from sat.core.constants import Const as C 27 from sat.core.constants import Const as C
22 from sat.core.log import getLogger 28 from sat.core.log import getLogger
23 from twisted.internet import defer 29 from sat.core.xmpp import SatXMPPEntity
24 from twisted.words.protocols.jabber import jid 30 from sat.tools import utils
25 from wokkel import disco, iwokkel
26 from zope.interface import implementer
27 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
28 31
29 log = getLogger(__name__) 32 log = getLogger(__name__)
30 33
31 34
32 PLUGIN_INFO = { 35 PLUGIN_INFO = {
121 .format(url=thumb_url)) 124 .format(url=thumb_url))
122 else: 125 else:
123 invitation_elt['thumb_url'] = thumb_url 126 invitation_elt['thumb_url'] = thumb_url
124 return mess_data, invitation_elt 127 return mess_data, invitation_elt
125 128
126 def sendPubsubInvitation(self, client, invitee_jid, service, node, 129 def sendPubsubInvitation(
127 item_id, name, extra): 130 self,
131 client: SatXMPPEntity,
132 invitee_jid: jid.JID,
133 service: jid.JID,
134 node: str,
135 item_id: Optional[str],
136 name: Optional[str],
137 extra: Optional[dict]
138 ) -> None:
128 """Send an pubsub invitation in a <message> stanza 139 """Send an pubsub invitation in a <message> stanza
129 140
130 @param invitee_jid(jid.JID): entitee to send invitation to 141 @param invitee_jid: entitee to send invitation to
131 @param service(jid.JID): pubsub service 142 @param service: pubsub service
132 @param node(unicode): pubsub node 143 @param node: pubsub node
133 @param item_id(unicode): pubsub id 144 @param item_id: pubsub id
134 @param name(unicode, None): see [_generateBaseInvitation] 145 None when the invitation is for a whole node
135 @param extra(dict, None): see [_generateBaseInvitation] 146 @param name: see [_generateBaseInvitation]
147 @param extra: see [_generateBaseInvitation]
136 """ 148 """
137 if extra is None: 149 if extra is None:
138 extra = {} 150 extra = {}
139 mess_data, invitation_elt = self._generateBaseInvitation( 151 mess_data, invitation_elt = self._generateBaseInvitation(
140 client, invitee_jid, name, extra) 152 client, invitee_jid, name, extra)
141 pubsub_elt = invitation_elt.addElement("pubsub") 153 pubsub_elt = invitation_elt.addElement("pubsub")
142 pubsub_elt["service"] = service.full() 154 pubsub_elt["service"] = service.full()
143 pubsub_elt["node"] = node 155 pubsub_elt["node"] = node
144 pubsub_elt["item"] = item_id 156 if item_id is None:
145 return client.send(mess_data["xml"]) 157 try:
158 namespace = extra.pop("namespace")
159 except KeyError:
160 raise exceptions.DataError('"namespace" key is missing in "extra" data')
161 node_data_elt = pubsub_elt.addElement("node_data")
162 node_data_elt["namespace"] = namespace
163 try:
164 node_data_elt.addChild(extra["element"])
165 except KeyError:
166 pass
167 else:
168 pubsub_elt["item"] = item_id
169 if "element" in extra:
170 invitation_elt.addChild(extra.pop("element"))
171 client.send(mess_data["xml"])
146 172
147 async def sendFileSharingInvitation( 173 async def sendFileSharingInvitation(
148 self, client, invitee_jid, service, repos_type=None, namespace=None, path=None, 174 self, client, invitee_jid, service, repos_type=None, namespace=None, path=None,
149 name=None, extra=None 175 name=None, extra=None
150 ): 176 ):
205 file_sharing_elt["namespace"] = namespace 231 file_sharing_elt["namespace"] = namespace
206 if path is not None: 232 if path is not None:
207 file_sharing_elt["path"] = path 233 file_sharing_elt["path"] = path
208 client.send(mess_data["xml"]) 234 client.send(mess_data["xml"])
209 235
210 @defer.inlineCallbacks 236 async def _parsePubsubElt(self, client, pubsub_elt):
211 def _parsePubsubElt(self, client, pubsub_elt):
212 try: 237 try:
213 service = jid.JID(pubsub_elt["service"]) 238 service = jid.JID(pubsub_elt["service"])
214 node = pubsub_elt["node"] 239 node = pubsub_elt["node"]
215 item_id = pubsub_elt.getAttribute("item")
216 except (RuntimeError, KeyError): 240 except (RuntimeError, KeyError):
217 log.warning(_("Bad invitation, ignoring")) 241 raise exceptions.DataError("Bad invitation, ignoring")
218 raise exceptions.DataError 242
219 243 item_id = pubsub_elt.getAttribute("item")
220 try: 244
221 items, metadata = yield self._p.getItems(client, service, node, 245 if item_id is not None:
222 item_ids=[item_id]) 246 try:
223 except Exception as e: 247 items, metadata = await self._p.getItems(
224 log.warning(_("Can't get item linked with invitation: {reason}").format( 248 client, service, node, item_ids=[item_id]
225 reason=e)) 249 )
226 try: 250 except Exception as e:
227 item_elt = items[0] 251 log.warning(_("Can't get item linked with invitation: {reason}").format(
228 except IndexError: 252 reason=e))
229 log.warning(_("Invitation was linking to a non existing item")) 253 try:
230 raise exceptions.DataError 254 item_elt = items[0]
231 255 except IndexError:
232 try: 256 log.warning(_("Invitation was linking to a non existing item"))
233 namespace = item_elt.firstChildElement().uri 257 raise exceptions.DataError
234 except Exception as e: 258
235 log.warning(_("Can't retrieve namespace of invitation: {reason}").format( 259 try:
236 reason = e)) 260 namespace = item_elt.firstChildElement().uri
237 raise exceptions.DataError 261 except Exception as e:
238 262 log.warning(_("Can't retrieve namespace of invitation: {reason}").format(
239 args = [service, node, item_id, item_elt] 263 reason = e))
240 defer.returnValue((namespace, args)) 264 raise exceptions.DataError
241 265
242 def _parseFileSharingElt(self, client, file_sharing_elt): 266 args = [service, node, item_id, item_elt]
267 else:
268 try:
269 node_data_elt = next(pubsub_elt.elements((NS_INVITATION, "node_data")))
270 except StopIteration:
271 raise exceptions.DataError("Bad invitation, ignoring")
272 namespace = node_data_elt['namespace']
273 args = [service, node, None, node_data_elt]
274
275 return namespace, args
276
277 async def _parseFileSharingElt(self, client, file_sharing_elt):
243 try: 278 try:
244 service = jid.JID(file_sharing_elt["service"]) 279 service = jid.JID(file_sharing_elt["service"])
245 except (RuntimeError, KeyError): 280 except (RuntimeError, KeyError):
246 log.warning(_("Bad invitation, ignoring")) 281 log.warning(_("Bad invitation, ignoring"))
247 raise exceptions.DataError 282 raise exceptions.DataError
248 repos_type = file_sharing_elt.getAttribute("type", "files") 283 repos_type = file_sharing_elt.getAttribute("type", "files")
249 namespace = file_sharing_elt.getAttribute("namespace") 284 sharing_ns = file_sharing_elt.getAttribute("namespace")
250 path = file_sharing_elt.getAttribute("path") 285 path = file_sharing_elt.getAttribute("path")
251 args = [service, repos_type, namespace, path] 286 args = [service, repos_type, sharing_ns, path]
252 ns_fis = self.host.getNamespace("fis") 287 ns_fis = self.host.getNamespace("fis")
253 return ns_fis, args 288 return ns_fis, args
254 289
255 @defer.inlineCallbacks 290 async def onInvitation(self, message_elt, client):
256 def onInvitation(self, message_elt, client):
257 log.debug("invitation received [{profile}]".format(profile=client.profile)) 291 log.debug("invitation received [{profile}]".format(profile=client.profile))
258 invitation_elt = message_elt.invitation 292 invitation_elt = message_elt.invitation
259 293
260 name = invitation_elt.getAttribute("name") 294 name = invitation_elt.getAttribute("name")
261 extra = {} 295 extra = {}
273 else: 307 else:
274 log.warning("not implemented invitation element: {xml}".format( 308 log.warning("not implemented invitation element: {xml}".format(
275 xml = elt.toXml())) 309 xml = elt.toXml()))
276 continue 310 continue
277 try: 311 try:
278 namespace, args = yield method(client, elt) 312 namespace, args = await method(client, elt)
279 except exceptions.DataError: 313 except exceptions.DataError:
280 log.warning("Can't parse invitation element: {xml}".format( 314 log.warning("Can't parse invitation element: {xml}".format(
281 xml = elt.toXml())) 315 xml = elt.toXml()))
282 continue 316 continue
283 317
286 except KeyError: 320 except KeyError:
287 log.warning(_( 321 log.warning(_(
288 'No handler for namespace "{namespace}", invitation ignored') 322 'No handler for namespace "{namespace}", invitation ignored')
289 .format(namespace=namespace)) 323 .format(namespace=namespace))
290 else: 324 else:
291 cb(client, name, extra, *args) 325 await utils.asDeferred(cb, client, namespace, name, extra, *args)
292 326
293 327
294 @implementer(iwokkel.IDisco) 328 @implementer(iwokkel.IDisco)
295 class PubsubInvitationHandler(XMPPHandler): 329 class PubsubInvitationHandler(XMPPHandler):
296 330
297 def __init__(self, plugin_parent): 331 def __init__(self, plugin_parent):
298 self.plugin_parent = plugin_parent 332 self.plugin_parent = plugin_parent
299 333
300 def connectionInitialized(self): 334 def connectionInitialized(self):
301 self.xmlstream.addObserver( 335 self.xmlstream.addObserver(
302 INVITATION, self.plugin_parent.onInvitation, client=self.parent 336 INVITATION,
337 lambda message_elt: defer.ensureDeferred(
338 self.plugin_parent.onInvitation(message_elt, client=self.parent)
339 ),
303 ) 340 )
304 341
305 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): 342 def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
306 return [ 343 return [
307 disco.DiscoFeature(NS_INVITATION), 344 disco.DiscoFeature(NS_INVITATION),