diff libervia/backend/plugins/plugin_comp_email_gateway/pubsub_service.py @ 4386:c055042c01e3

component Email gateway: Convert mailing list to pubsub nodes: - `Credentials` is now a Pydantic based model. - Mailing list related emails are now detected hand saved as pubsub items, using XEP-0277 blog items, and creating suitable nodes. The ID of the mailing list is used for root node. - Mailing list items are currently restricted to recipient associated JID. - Words in square bracket in title `[Like][That]` are removed and converted to blog categories. - Method to convert between MbData and email (in both directions) have been created. rel 462
author Goffi <goffi@goffi.org>
date Sun, 03 Aug 2025 23:45:48 +0200
parents 699aa8788d98
children
line wrap: on
line diff
--- a/libervia/backend/plugins/plugin_comp_email_gateway/pubsub_service.py	Sun Aug 03 23:45:45 2025 +0200
+++ b/libervia/backend/plugins/plugin_comp_email_gateway/pubsub_service.py	Sun Aug 03 23:45:48 2025 +0200
@@ -16,18 +16,22 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, cast
+
 from twisted.internet import defer
-from twisted.words.protocols.jabber import jid, error
+from twisted.words.protocols.jabber import error, jid
 from twisted.words.xish import domish
 from wokkel import data_form, disco, pubsub, rsm
 
+from libervia.backend import G
+from libervia.backend.core import exceptions
+from libervia.backend.core.core_types import SatXMPPComponent
 from libervia.backend.core.i18n import _
-from libervia.backend.core.constants import Const as C
 from libervia.backend.core.log import getLogger
+from libervia.backend.memory.sqla_mapping import AccessModel, Affiliation
+from libervia.backend.plugins.plugin_pubsub_cache import PubsubCache
 from libervia.backend.plugins.plugin_xep_0498 import NodeData
 from libervia.backend.tools.utils import ensure_deferred
-
 if TYPE_CHECKING:
     from . import EmailGatewayComponent
 
@@ -57,31 +61,106 @@
         self.host = self.gateway.host
         self.service = service
         self._pfs = service._pfs
+        self._ps_cache = cast(PubsubCache, G.host.plugins["PUBSUB_CACHE"])
         super().__init__()
 
+    @property
+    def client(self) -> SatXMPPComponent:
+        client = self.gateway.client
+        assert client is not None
+        return client
+
     def getNodes(
         self, requestor: jid.JID, service: jid.JID, nodeIdentifier: str
     ) -> defer.Deferred[list[str]]:
-        return defer.succeed([self._pfs.namespace])
+        return defer.ensureDeferred(self.get_nodes(requestor, service, nodeIdentifier))
+
+    async def get_nodes(
+        self,
+        requestor: jid.JID,
+        service: jid.JID,
+        node_id: str
+    ) -> list[str]:
+        nodes = await G.storage.get_pubsub_nodes(self.client, self.client.jid)
+        return [self._pfs.namespace] + [cast(str, node.name) for node in nodes]
 
     @ensure_deferred
     async def items(
         self,
         request: rsm.PubSubRequest,
     ) -> tuple[list[domish.Element], rsm.RSMResponse | None]:
-        client = self.gateway.client
-        assert client is not None
-        sender = request.sender.userhostJID()
-        if not client.is_local(sender):
+        client = self.client
+        requestor_jid = request.sender.userhostJID()
+        if not client.is_local(requestor_jid):
             raise error.StanzaError("forbidden")
 
         if request.nodeIdentifier != self._pfs.namespace:
-            return [], None
+            return await self.items_from_mailing_list(request, requestor_jid)
 
-        files = await self.host.memory.get_files(client, sender)
+        files = await self.host.memory.get_files(client, requestor_jid)
         node_data = NodeData.from_files_data(client.jid, files)
         return node_data.to_elements(), None
 
+    async def items_from_mailing_list(
+        self,
+        request: rsm.PubSubRequest,
+        requestor_jid: jid.JID
+    ) -> tuple[list[domish.Element], rsm.RSMResponse|None]:
+        """Handle items coming from mailing lists.
+
+        @param request: Pubsub request.
+        @param requestor_jid: Bare jid of the requestor.
+        @return: Items matching request, if allowed, and RSM response.
+        @raise error.StanzaError: One of:
+            - ``item-not-found`` if no corresponding node or item if found
+            - ``forbidden`` if the requestor does not have sufficient privileges
+        """
+        node = request.nodeIdentifier
+        node = await G.storage.get_pubsub_node(
+            self.client,
+            self.client.jid,
+            node,
+            with_affiliations=True
+        )
+        if node is None:
+            raise error.StanzaError("item-not-found")
+
+        match str(node.access_model):
+            case AccessModel.open:
+                pass
+            case AccessModel.whitelist:
+                for affiliation in node.affiliations:
+                    if (
+                        affiliation.entity == requestor_jid
+                        and affiliation.affiliation in {
+                            Affiliation.owner, Affiliation.publisher, Affiliation.member
+                        }
+                    ):
+                        break
+                else:
+                    raise error.StanzaError("forbidden")
+            case _:
+                raise exceptions.InternalError(
+                    f"Unmanaged access model: {node.access_model}"
+                )
+
+        pubsub_items, metadata = await self._ps_cache.get_items_from_cache(
+            self.client,
+            node,
+            request.maxItems,
+            request.itemIdentifiers,
+            request.subscriptionIdentifier,
+            request.rsm
+        )
+        if rsm_data := metadata.get("rsm"):
+            rsm_response = rsm.RSMResponse(**rsm_data)
+        else:
+            rsm_response = None
+        return (
+            [cast(domish.Element, ps_item.data) for ps_item in pubsub_items],
+            rsm_response
+        )
+
     @ensure_deferred
     async def retract(self, request: rsm.PubSubRequest) -> None:
         client = self.gateway.client