changeset 3832:201a22bfbb74

component AP gateway: convert AP mention to XEP-0372 mentions: when a mentions are found in AP items (either with people specified directly as target, of with `mention` tags as in https://www.w3.org/TR/activitystreams-vocabulary/#microsyntaxes), they are converted to XEP-0372 `mention` references. This is only done for Pubsub items (i.e. not for private messages). rel 369
author Goffi <goffi@goffi.org>
date Sun, 10 Jul 2022 15:16:15 +0200
parents 604b6acaee22
children 381340b9a9ee
files sat/plugins/plugin_comp_ap_gateway/__init__.py sat/plugins/plugin_comp_ap_gateway/constants.py
diffstat 2 files changed, 103 insertions(+), 16 deletions(-) [+]
line wrap: on
line diff
--- a/sat/plugins/plugin_comp_ap_gateway/__init__.py	Sun Jul 10 15:16:15 2022 +0200
+++ b/sat/plugins/plugin_comp_ap_gateway/__init__.py	Sun Jul 10 15:16:15 2022 +0200
@@ -70,6 +70,7 @@
     TYPE_ITEM,
     TYPE_FOLLOWERS,
     TYPE_TOMBSTONE,
+    TYPE_MENTION,
     NS_AP_PUBLIC,
     PUBLIC_TUPLE
 )
@@ -89,7 +90,8 @@
     C.PI_PROTOCOLS: [],
     C.PI_DEPENDENCIES: [
         "XEP-0060", "XEP-0084", "XEP-0106", "XEP-0277", "XEP-0292", "XEP-0329",
-        "XEP-0424", "XEP-0465", "PUBSUB_CACHE", "TEXT_SYNTAXES", "IDENTITY", "XEP-0054"
+        "XEP-0372", "XEP-0424", "XEP-0465", "PUBSUB_CACHE", "TEXT_SYNTAXES", "IDENTITY",
+        "XEP-0054"
     ],
     C.PI_RECOMMENDATIONS: [],
     C.PI_MAIN: "APGateway",
@@ -117,6 +119,7 @@
         self._e = host.plugins["XEP-0106"]
         self._m = host.plugins["XEP-0277"]
         self._v = host.plugins["XEP-0292"]
+        self._refs = host.plugins["XEP-0372"]
         self._r = host.plugins["XEP-0424"]
         self._pps = host.plugins["XEP-0465"]
         self._c = host.plugins["PUBSUB_CACHE"]
@@ -1694,6 +1697,8 @@
         self,
         client: SatXMPPEntity,
         ap_item: dict,
+        targets: Dict[str, Set[str]],
+        mentions: List[Dict[str, str]],
     ) -> None:
         """We got an AP item which is a reply to an XMPP item"""
         in_reply_to = ap_item["inReplyTo"]
@@ -1741,17 +1746,22 @@
         else:
             __, item_elt = await self.apItem2MbDataAndElt(ap_item)
             await self._p.publish(client, comment_service, comment_node, [item_elt])
+            await self.notifyMentions(
+                targets, mentions, comment_service, comment_node, item_elt["id"]
+            )
 
-    def getAPItemTargets(self, item: Dict[str, Any]) -> Tuple[bool, Set[str], Set[str]]:
+    def getAPItemTargets(
+        self,
+        item: Dict[str, Any]
+    ) -> Tuple[bool, Dict[str, Set[str]], List[Dict[str, str]]]:
         """Retrieve targets of an AP item, and indicate if it's a public one
 
         @param item: AP object payload
         @return: Are returned:
             - is_public flag, indicating if the item is world-readable
-            - targets of the item
-            - targets of the items
+            - a dict mapping target type to targets
         """
-        targets: Set[str] = set()
+        targets: Dict[str, Set[str]] = {}
         is_public = False
         # TODO: handle "audience"
         for key in ("to", "bto", "cc", "bcc"):
@@ -1768,10 +1778,35 @@
                     continue
                 if not self.isLocalURL(value):
                     continue
-                targets.add(value)
+                target_type = self.parseAPURL(value)[0]
+                if target_type != TYPE_ACTOR:
+                    log.debug(f"ignoring non actor type as a target: {href}")
+                else:
+                    targets.setdefault(target_type, set()).add(value)
 
-        targets_types = {self.parseAPURL(t)[0] for t in targets}
-        return is_public, targets, targets_types
+        mentions = []
+        tags = item.get("tag")
+        if tags:
+            for tag in tags:
+                if tag.get("type") != TYPE_MENTION:
+                    continue
+                href = tag.get("href")
+                if not href:
+                    log.warning('Missing "href" field from mention object: {tag!r}')
+                    continue
+                if not self.isLocalURL(href):
+                    continue
+                uri_type = self.parseAPURL(href)[0]
+                if uri_type != TYPE_ACTOR:
+                    log.debug(f"ignoring non actor URI as a target: {href}")
+                    continue
+                mention = {"uri": href}
+                mentions.append(mention)
+                name = tag.get("name")
+                if name:
+                    mention["content"] = name
+
+        return is_public, targets, mentions
 
     async def newAPItem(
         self,
@@ -1786,21 +1821,22 @@
         @param node: XMPP pubsub node
         @param item: AP object payload
         """
-        is_public, targets, targets_types = self.getAPItemTargets(item)
-        if not is_public and targets_types == {TYPE_ACTOR}:
+        is_public, targets, mentions = self.getAPItemTargets(item)
+        if not is_public and targets.keys() == {TYPE_ACTOR}:
             # this is a direct message
             await self.handleMessageAPItem(
-                client, targets, destinee, item
+                client, targets, mentions, destinee, item
             )
         else:
             await self.handlePubsubAPItem(
-                client, targets, destinee, node, item, is_public
+                client, targets, mentions, destinee, node, item, is_public
             )
 
     async def handleMessageAPItem(
         self,
         client: SatXMPPEntity,
-        targets: Set[str],
+        targets: Dict[str, Set[str]],
+        mentions: List[Dict[str, str]],
         destinee: Optional[jid.JID],
         item: dict,
     ) -> None:
@@ -1810,7 +1846,11 @@
         @param destinee: jid of the destinee,
         @param item: AP object payload
         """
-        targets_jids = {await self.getJIDFromId(t) for t in targets}
+        targets_jids = {
+            await self.getJIDFromId(t)
+            for t_set in targets.values()
+            for t in t_set
+        }
         if destinee is not None:
             targets_jids.add(destinee)
         mb_data = await self.apItem2MBdata(item)
@@ -1826,10 +1866,53 @@
             )
         await defer.DeferredList(defer_l)
 
+    async def notifyMentions(
+        self,
+        targets: Dict[str, Set[str]],
+        mentions: List[Dict[str, str]],
+        service: jid.JID,
+        node: str,
+        item_id: str,
+    ) -> None:
+        """Send mention notifications to recipients and mentioned entities
+
+        XEP-0372 (References) is used.
+
+        Mentions are also sent to recipients as they are primary audience (see
+        https://www.w3.org/TR/activitystreams-vocabulary/#microsyntaxes).
+
+        """
+        anchor = uri.buildXMPPUri("pubsub", path=service.full(), node=node, item=item_id)
+        seen = set()
+        # we start with explicit mentions because mentions' content will be used in the
+        # future to fill "begin" and "end" reference attributes (we can't do it at the
+        # moment as there is no way to specify the XML element to use in the blog item).
+        for mention in mentions:
+            mentioned_jid = await self.getJIDFromId(mention["uri"])
+            self._refs.sendReference(
+                self.client,
+                to_jid=mentioned_jid,
+                anchor=anchor
+            )
+            seen.add(mentioned_jid)
+
+        remaining = {
+            await self.getJIDFromId(t)
+            for t_set in targets.values()
+            for t in t_set
+        } - seen
+        for target in remaining:
+            self._refs.sendReference(
+                self.client,
+                to_jid=target,
+                anchor=anchor
+            )
+
     async def handlePubsubAPItem(
         self,
         client: SatXMPPEntity,
-        targets: Set[str],
+        targets: Dict[str, Set[str]],
+        mentions: List[Dict[str, str]],
         destinee: Optional[jid.JID],
         node: str,
         item: dict,
@@ -1853,7 +1936,8 @@
         if in_reply_to and isinstance(in_reply_to, str):
             if self.isLocalURL(in_reply_to):
                 # this is a reply to an XMPP item
-                return await self.newReplyToXMPPItem(client, item)
+                await self.newReplyToXMPPItem(client, item, targets, mentions)
+                return
 
             # this item is a reply to an AP item, we use or create a corresponding node
             # for comments
@@ -1910,6 +1994,8 @@
                 [(subscription.subscriber, None, [item_elt])]
             )
 
+        await self.notifyMentions(targets, mentions, service, node, item_elt["id"])
+
     async def newAPDeleteItem(
         self,
         client: SatXMPPEntity,
--- a/sat/plugins/plugin_comp_ap_gateway/constants.py	Sun Jul 10 15:16:15 2022 +0200
+++ b/sat/plugins/plugin_comp_ap_gateway/constants.py	Sun Jul 10 15:16:15 2022 +0200
@@ -28,6 +28,7 @@
 TYPE_FOLLOWING = "following"
 TYPE_ITEM = "item"
 TYPE_TOMBSTONE = "Tombstone"
+TYPE_MENTION = "Mention"
 MEDIA_TYPE_AP = "application/activity+json"
 NS_AP_PUBLIC = "https://www.w3.org/ns/activitystreams#Public"
 # 3 values can be used, see https://www.w3.org/TR/activitypub/#public-addressing