comparison libervia/backend/plugins/plugin_comp_email_gateway/pubsub_service.py @ 4338:7c0b7ecb816f

component email gateway: Add a pubsub service: a pubsub service is implemented to retrieve and manage attachments using XEP-0498. rel 453
author Goffi <goffi@goffi.org>
date Tue, 03 Dec 2024 00:13:23 +0100
parents
children 699aa8788d98
comparison
equal deleted inserted replaced
4337:95792a1f26c7 4338:7c0b7ecb816f
1 #!/usr/bin/env python3
2
3 # Libervia ActivityPub Gateway
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 pathlib import Path
20 from typing import TYPE_CHECKING
21 from twisted.internet import defer
22 from twisted.words.protocols.jabber import jid, error
23 from twisted.words.xish import domish
24 from wokkel import data_form, disco, pubsub, rsm
25
26 from libervia.backend.core.i18n import _
27 from libervia.backend.core.constants import Const as C
28 from libervia.backend.core.log import getLogger
29 from libervia.backend.plugins.plugin_xep_0498 import NodeData
30 from libervia.backend.tools.utils import ensure_deferred
31
32 if TYPE_CHECKING:
33 from . import EmailGatewayComponent
34
35
36 log = getLogger(__name__)
37
38 # all nodes have the same config
39 NODE_CONFIG = [
40 {"var": "pubsub#persist_items", "type": "boolean", "value": True},
41 {"var": "pubsub#max_items", "value": "max"},
42 {"var": "pubsub#access_model", "type": "list-single", "value": "open"},
43 {"var": "pubsub#publish_model", "type": "list-single", "value": "open"},
44 ]
45
46 NODE_CONFIG_VALUES = {c["var"]: c["value"] for c in NODE_CONFIG}
47 NODE_OPTIONS = {c["var"]: {} for c in NODE_CONFIG}
48 for c in NODE_CONFIG:
49 NODE_OPTIONS[c["var"]].update(
50 {k: v for k, v in c.items() if k not in ("var", "value")}
51 )
52
53
54 class EmailGWPubsubResource(pubsub.PubSubResource):
55
56 def __init__(self, service: "EmailGWPubsubService") -> None:
57 self.gateway = service.gateway
58 self.host = self.gateway.host
59 self.service = service
60 self._pfs = service._pfs
61 super().__init__()
62
63 def getNodes(
64 self, requestor: jid.JID, service: jid.JID, nodeIdentifier: str
65 ) -> defer.Deferred[list[str]]:
66 return defer.succeed([self._pfs.namespace])
67
68 @ensure_deferred
69 async def items(
70 self,
71 request: rsm.PubSubRequest,
72 ) -> tuple[list[domish.Element], rsm.RSMResponse | None]:
73 client = self.gateway.client
74 assert client is not None
75 sender = request.sender.userhostJID()
76 if not client.is_local(sender):
77 raise error.StanzaError("forbidden")
78
79 if request.nodeIdentifier != self._pfs.namespace:
80 return [], None
81
82 files = await self.host.memory.get_files(client, sender)
83 node_data = NodeData.from_files_data(client.jid, files)
84 return node_data.to_elements(), None
85
86 @ensure_deferred
87 async def retract(self, request: rsm.PubSubRequest) -> None:
88 client = self.gateway.client
89 assert client is not None
90 sender = request.sender.userhostJID()
91 if not client.is_local(sender):
92 raise error.StanzaError("forbidden")
93 if request.nodeIdentifier != self._pfs.namespace:
94 raise error.StanzaError("bad-request")
95
96 for item_id in request.itemIdentifiers:
97 try:
98 # FIXME: item ID naming convention must be hanlded using dedicated methods
99 # in XEP-0498.
100 file_id = item_id.rsplit("_", 1)[1]
101 except IndexError:
102 file_id = ""
103 if not file_id:
104 raise error.StanzaError("bad-request")
105 # Ownership is checked by ``file_delete``, and payload deletion is done there
106 # too.
107 await self.host.memory.file_delete(client, sender.userhostJID(), file_id)
108
109 @ensure_deferred
110 async def subscribe(self, request: rsm.PubSubRequest):
111 raise rsm.Unsupported("subscribe")
112
113 @ensure_deferred
114 async def unsubscribe(self, request: rsm.PubSubRequest):
115 raise rsm.Unsupported("unsubscribe")
116
117 def getConfigurationOptions(self):
118 return NODE_OPTIONS
119
120 def getConfiguration(
121 self, requestor: jid.JID, service: jid.JID, nodeIdentifier: str
122 ) -> defer.Deferred:
123 return defer.succeed(NODE_CONFIG_VALUES)
124
125 def getNodeInfo(
126 self,
127 requestor: jid.JID,
128 service: jid.JID,
129 nodeIdentifier: str,
130 pep: bool = False,
131 recipient: jid.JID | None = None,
132 ) -> dict | None:
133 if not nodeIdentifier:
134 return None
135 info = {"type": "leaf", "meta-data": NODE_CONFIG}
136 return info
137
138
139 class EmailGWPubsubService(rsm.PubSubService):
140 """Pubsub service for XMPP requests"""
141
142 def __init__(self, gateway: "EmailGatewayComponent"):
143 self.gateway = gateway
144 self._pfs = gateway._pfs
145 resource = EmailGWPubsubResource(self)
146 super().__init__(resource)
147 self.host = gateway.host
148 self.discoIdentity = {
149 "category": "pubsub",
150 "type": "service",
151 "name": "Libervia Email Gateway",
152 }
153
154 @ensure_deferred
155 async def getDiscoInfo(
156 self, requestor: jid.JID, target: jid.JID, nodeIdentifier: str = ""
157 ) -> list[disco.DiscoFeature | disco.DiscoIdentity | data_form.Form]:
158 infos = await super().getDiscoInfo(requestor, target, nodeIdentifier)
159 infos.append(disco.DiscoFeature(self._pfs.namespace))
160 return infos