diff sat/plugins/plugin_comp_ap_gateway/__init__.py @ 4023:78b5f356900c

component AP gateway: handle attachments
author Goffi <goffi@goffi.org>
date Thu, 23 Mar 2023 15:42:21 +0100
parents 4a2c261646b6
children 44abce96ac6b
line wrap: on
line diff
--- a/sat/plugins/plugin_comp_ap_gateway/__init__.py	Thu Mar 23 15:39:48 2023 +0100
+++ b/sat/plugins/plugin_comp_ap_gateway/__init__.py	Thu Mar 23 15:42:21 2023 +0100
@@ -109,7 +109,7 @@
     C.PI_DEPENDENCIES: [
         "XEP-0050", "XEP-0054", "XEP-0060", "XEP-0084", "XEP-0106", "XEP-0277",
         "XEP-0292", "XEP-0329", "XEP-0372", "XEP-0424", "XEP-0465", "XEP-0470",
-        "PUBSUB_CACHE", "TEXT_SYNTAXES", "IDENTITY", "EVENTS"
+        "XEP-0447", "PUBSUB_CACHE", "TEXT_SYNTAXES", "IDENTITY", "EVENTS"
     ],
     C.PI_RECOMMENDATIONS: [],
     C.PI_MAIN: "APGateway",
@@ -131,7 +131,7 @@
     # 1: log POST objects
     # 2: log POST and GET objects
     # 3: log POST and GET objects with HTTP headers for GET requests
-    verbose = 0
+    verbose = 3
 
     def __init__(self, host):
         self.host = host
@@ -144,11 +144,12 @@
         self._v = host.plugins["XEP-0292"]
         self._refs = host.plugins["XEP-0372"]
         self._r = host.plugins["XEP-0424"]
+        self._sfs = host.plugins["XEP-0447"]
         self._pps = host.plugins["XEP-0465"]
+        self._pa = host.plugins["XEP-0470"]
         self._c = host.plugins["PUBSUB_CACHE"]
         self._t = host.plugins["TEXT_SYNTAXES"]
         self._i = host.plugins["IDENTITY"]
-        self._pa = host.plugins["XEP-0470"]
         self._events = host.plugins["EVENTS"]
         self._p.addManagedNode(
             "",
@@ -487,7 +488,7 @@
                 return ap_item["object"]
             else:
                 # this is a blog item
-                mb_data = await self._m.item2mbdata(
+                mb_data = await self._m.item_2_mb_data(
                     self.client, found_item, author_jid, node
                 )
                 ap_item = await self.mb_data_2_ap_item(self.client, mb_data)
@@ -901,7 +902,7 @@
         pub_key = serialization.load_pem_public_key(pub_key_pem.encode())
         return key_id, owner, pub_key
 
-    def createActivity(
+    def create_activity(
         self,
         activity: str,
         actor_id: str,
@@ -1094,7 +1095,7 @@
                     )
                 else:
                     # blog item
-                    mb_data = await self._m.item2mbdata(client, item, service, node)
+                    mb_data = await self._m.item_2_mb_data(client, item, service, node)
                     author_jid = jid.JID(mb_data["author_jid"])
                     if subscribe_extra_nodes and not self.isVirtualJID(author_jid):
                         # we subscribe automatically to comment nodes if any
@@ -1232,7 +1233,7 @@
             if not "noticed" in old_attachment:
                 # new "noticed" attachment, we translate to "Like" activity
                 activity_id = self.buildAPURL("like", item_account, item_id)
-                activity = self.createActivity(
+                activity = self.create_activity(
                     TYPE_LIKE, publisher_actor_id, item_url, activity_id=activity_id
                 )
                 activity["to"] = [ap_account]
@@ -1242,12 +1243,12 @@
             if "noticed" in old_attachment:
                 # "noticed" attachment has been removed, we undo the "Like" activity
                 activity_id = self.buildAPURL("like", item_account, item_id)
-                activity = self.createActivity(
+                activity = self.create_activity(
                     TYPE_LIKE, publisher_actor_id, item_url, activity_id=activity_id
                 )
                 activity["to"] = [ap_account]
                 activity["cc"] = [NS_AP_PUBLIC]
-                undo = self.createActivity("Undo", publisher_actor_id, activity)
+                undo = self.create_activity("Undo", publisher_actor_id, activity)
                 await self.signAndPost(inbox, publisher_actor_id, undo)
 
         # reactions
@@ -1260,7 +1261,7 @@
                 activity_id = self.buildAPURL(
                     "reaction", item_account, item_id, reaction.encode().hex()
                 )
-                reaction_activity = self.createActivity(
+                reaction_activity = self.create_activity(
                     TYPE_REACTION, publisher_actor_id, item_url,
                     activity_id=activity_id
                 )
@@ -1268,7 +1269,7 @@
                 reaction_activity["to"] = [ap_account]
                 reaction_activity["cc"] = [NS_AP_PUBLIC]
                 if undo:
-                    activy = self.createActivity(
+                    activy = self.create_activity(
                         "Undo", publisher_actor_id, reaction_activity
                     )
                 else:
@@ -1282,7 +1283,7 @@
             if attending != old_attending:
                 activity_type = TYPE_JOIN if attending == "yes" else TYPE_LEAVE
                 activity_id = self.buildAPURL(activity_type.lower(), item_account, item_id)
-                activity = self.createActivity(
+                activity = self.create_activity(
                     activity_type, publisher_actor_id, item_url, activity_id=activity_id
                 )
                 activity["to"] = [ap_account]
@@ -1293,7 +1294,7 @@
                 old_attending = old_attachment.get("rsvp", {}).get("attending", "no")
                 if old_attending == "yes":
                     activity_id = self.buildAPURL(TYPE_LEAVE.lower(), item_account, item_id)
-                    activity = self.createActivity(
+                    activity = self.create_activity(
                         TYPE_LEAVE, publisher_actor_id, item_url, activity_id=activity_id
                     )
                     activity["to"] = [ap_account]
@@ -1645,8 +1646,8 @@
 
     async def ap_item_2_mb_data_and_elt(self, ap_item: dict) -> Tuple[dict, domish.Element]:
         """Convert AP item to parsed microblog data and corresponding item element"""
-        mb_data = await self.apItem2MBdata(ap_item)
-        item_elt = await self._m.data2entry(
+        mb_data = await self.ap_item_2_mb_data(ap_item)
+        item_elt = await self._m.mb_data_2_entry_elt(
             self.client, mb_data, mb_data["id"], None, self._m.namespace
         )
         if "repeated" in mb_data["extra"]:
@@ -1742,7 +1743,7 @@
                 None
             )
 
-    async def apItem2MBdata(self, ap_item: dict) -> dict:
+    async def ap_item_2_mb_data(self, ap_item: dict) -> dict:
         """Convert AP activity or object to microblog data
 
         @param ap_item: ActivityPub item to convert
@@ -1786,6 +1787,32 @@
             False,
         )
 
+        if "attachment" in ap_object:
+            attachments = mb_data["extra"][C.KEY_ATTACHMENTS] = []
+            for ap_attachment in ap_object["attachment"]:
+                try:
+                    url = ap_attachment["url"]
+                except KeyError:
+                    log.warning(
+                        f'"url" missing in AP attachment, ignoring: {ap_attachment}'
+                    )
+                    continue
+
+                if not url.startswith("http"):
+                    log.warning(f"non HTTP URL in attachment, ignoring: {ap_attachment}")
+                    continue
+                attachment = {"url": url}
+                for ap_key, key in (
+                    ("mediaType", "media_type"),
+                    # XXX: as weird as it seems, "name" is actually used for description
+                    #   in AP world
+                    ("name", "desc"),
+                ):
+                    value = ap_attachment.get(ap_key)
+                    if value:
+                        attachment[key] = value
+                attachments.append(attachment)
+
         # author
         if is_activity:
             authors = await self.apGetActors(ap_item, "actor")
@@ -1899,7 +1926,7 @@
             TYPE_ITEM, parent_ap_account, parent_item
         )
 
-    async def repeatedMB2APItem(
+    async def repeated_mb_2_ap_item(
         self,
         mb_data: dict
     ) -> dict:
@@ -1952,7 +1979,7 @@
             )
             announced_uri = self.buildAPURL("item", repeated_account, rep_item)
 
-        announce = self.createActivity(
+        announce = self.create_activity(
             "Announce", repeater_id, announced_uri, activity_id=activity_id
         )
         announce["to"] = [NS_AP_PUBLIC]
@@ -1988,7 +2015,7 @@
         """
         extra = mb_data.get("extra", {})
         if "repeated" in extra:
-            return await self.repeatedMB2APItem(mb_data)
+            return await self.repeated_mb_2_ap_item(mb_data)
         if not mb_data.get("id"):
             mb_data["id"] = shortuuid.uuid()
         if not mb_data.get("author_jid"):
@@ -2011,6 +2038,32 @@
         if language:
             ap_object["contentMap"] = {language: ap_object["content"]}
 
+        attachments = extra.get(C.KEY_ATTACHMENTS)
+        if attachments:
+            ap_attachments = ap_object["attachment"] = []
+            for attachment in attachments:
+                try:
+                    url = next(
+                        s['url'] for s in attachment["sources"] if 'url' in s
+                    )
+                except (StopIteration, KeyError):
+                    log.warning(
+                        f"Ignoring attachment without URL: {attachment}"
+                    )
+                    continue
+                ap_attachment = {
+                    "url": url
+                }
+                for key, ap_key in (
+                    ("media_type", "mediaType"),
+                    # XXX: yes "name", cf. [ap_item_2_mb_data]
+                    ("desc", "name"),
+                ):
+                    value = attachment.get(key)
+                    if value:
+                        ap_attachment[ap_key] = value
+                ap_attachments.append(ap_attachment)
+
         if public:
             ap_object["to"] = [NS_AP_PUBLIC]
             if self.auto_mentions:
@@ -2068,7 +2121,7 @@
                         mb_data
                     )
 
-        return self.createActivity(
+        return self.create_activity(
             "Create" if is_new else "Update", url_actor, ap_object, activity_id=url_item
         )
 
@@ -2144,12 +2197,12 @@
             )
         else:
             try:
-                mb_data = await self._m.item2mbdata(self.client, items[0].data, jid_, node)
+                mb_data = await self._m.item_2_mb_data(self.client, items[0].data, jid_, node)
                 if "repeated" in mb_data["extra"]:
                     # we are deleting a repeated item, we must translate this to an
                     # "Undo" of the "Announce" activity instead of a "Delete" one
-                    announce = await self.repeatedMB2APItem(mb_data)
-                    undo = self.createActivity("Undo", author_actor_id, announce)
+                    announce = await self.repeated_mb_2_ap_item(mb_data)
+                    undo = self.create_activity("Undo", author_actor_id, announce)
                     return author_actor_id, undo
             except Exception as e:
                 log.debug(
@@ -2158,7 +2211,7 @@
                 )
 
         url_item = self.buildAPURL(TYPE_ITEM, author_account, item_id)
-        ap_item = self.createActivity(
+        ap_item = self.create_activity(
             "Delete",
             author_actor_id,
             {
@@ -2223,6 +2276,12 @@
         if origin_id:
             # we need to use origin ID when present to be able to retract the message
             mb_data["id"] = origin_id
+        attachments = mess_data["extra"].get(C.KEY_ATTACHMENTS)
+        if attachments:
+            mb_data["extra"] = {
+                C.KEY_ATTACHMENTS: attachments
+            }
+
         client = self.client.getVirtualClient(mess_data["from"])
         ap_item = await self.mb_data_2_ap_item(client, mb_data, public=False)
         ap_object = ap_item["object"]
@@ -2339,7 +2398,7 @@
 
         cached_item = cached_items[0]
 
-        mb_data = await self._m.item2mbdata(
+        mb_data = await self._m.item_2_mb_data(
             client, cached_item.data, pubsub_service, pubsub_node
         )
         ap_item = await self.mb_data_2_ap_item(client, mb_data)
@@ -2396,7 +2455,7 @@
                 f"Can't find parent item at {parent_item_service} (node "
                 f"{parent_item_node!r})\n{pformat(ap_item)}")
             return
-        parent_item_parsed = await self._m.item2mbdata(
+        parent_item_parsed = await self._m.item_2_mb_data(
             client, parent_item_elt, parent_item_service, parent_item_node
         )
         try:
@@ -2488,7 +2547,7 @@
         is_public, targets, mentions = self.getAPItemTargets(item)
         if not is_public and targets.keys() == {TYPE_ACTOR}:
             # this is a direct message
-            await self.handleMessageAPItem(
+            await self.handle_message_ap_item(
                 client, targets, mentions, destinee, item
             )
         else:
@@ -2496,7 +2555,7 @@
                 client, targets, mentions, destinee, node, item, is_public
             )
 
-    async def handleMessageAPItem(
+    async def handle_message_ap_item(
         self,
         client: SatXMPPEntity,
         targets: Dict[str, Set[str]],
@@ -2517,7 +2576,14 @@
         }
         if destinee is not None:
             targets_jids.add(destinee)
-        mb_data = await self.apItem2MBdata(item)
+        mb_data = await self.ap_item_2_mb_data(item)
+        extra = {
+            "origin_id": mb_data["id"]
+        }
+        attachments = mb_data["extra"].get(C.KEY_ATTACHMENTS)
+        if attachments:
+            extra[C.KEY_ATTACHMENTS] = attachments
+
         defer_l = []
         for target_jid in targets_jids:
             defer_l.append(
@@ -2525,7 +2591,7 @@
                     target_jid,
                     {'': mb_data.get("content", "")},
                     mb_data.get("title"),
-                    extra={"origin_id": mb_data["id"]}
+                    extra=extra
                 )
             )
         await defer.DeferredList(defer_l)