Mercurial > libervia-backend
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