changeset 4023:78b5f356900c

component AP gateway: handle attachments
author Goffi <goffi@goffi.org>
date Thu, 23 Mar 2023 15:42:21 +0100
parents cdb7de398c85
children 4941cd102f93
files sat/core/constants.py sat/core/xmpp.py sat/plugins/plugin_comp_ap_gateway/__init__.py sat/plugins/plugin_comp_ap_gateway/events.py sat/plugins/plugin_comp_ap_gateway/http_server.py sat/plugins/plugin_comp_ap_gateway/pubsub_service.py sat/plugins/plugin_misc_attach.py sat/plugins/plugin_sec_aesgcm.py sat/plugins/plugin_xep_0277.py sat/plugins/plugin_xep_0447.py sat/plugins/plugin_xep_0448.py sat_frontends/jp/cmd_message.py sat_frontends/quick_frontend/quick_chat.py
diffstat 13 files changed, 168 insertions(+), 97 deletions(-) [+]
line wrap: on
line diff
--- a/sat/core/constants.py	Thu Mar 23 15:39:48 2023 +0100
+++ b/sat/core/constants.py	Thu Mar 23 15:42:21 2023 +0100
@@ -132,11 +132,6 @@
     MESS_KEY_ENCRYPTED = "encrypted"
     MESS_KEY_TRUSTED = "trusted"
 
-    MESS_KEY_ATTACHMENTS = "attachments"
-    MESS_KEY_ATTACHMENTS_MEDIA_TYPE = "media_type"
-    MESS_KEY_ATTACHMENTS_PREVIEW = "preview"
-    MESS_KEY_ATTACHMENTS_RESIZE = "resize"
-
     # File encryption algorithms
     ENC_AES_GCM = "AES-GCM"
 
@@ -368,6 +363,11 @@
     ## Common data keys ##
     KEY_THUMBNAILS = "thumbnails"
     KEY_PROGRESS_ID = "progress_id"
+    KEY_ATTACHMENTS = "attachments"
+    KEY_ATTACHMENTS_MEDIA_TYPE = "media_type"
+    KEY_ATTACHMENTS_PREVIEW = "preview"
+    KEY_ATTACHMENTS_RESIZE = "resize"
+
 
     ## Common extra keys/values ##
     KEY_ORDER_BY = "order_by"
--- a/sat/core/xmpp.py	Thu Mar 23 15:39:48 2023 +0100
+++ b/sat/core/xmpp.py	Thu Mar 23 15:42:21 2023 +0100
@@ -821,7 +821,7 @@
         """Return True if a message contain payload to show in frontends"""
         return (
             mess_data["message"] or mess_data["subject"]
-            or mess_data["extra"].get(C.MESS_KEY_ATTACHMENTS)
+            or mess_data["extra"].get(C.KEY_ATTACHMENTS)
             or mess_data["type"] == C.MESS_TYPE_INFO
         )
 
@@ -1361,16 +1361,16 @@
 
     def completeAttachments(self, data):
         """Complete missing metadata of attachments"""
-        for attachment in data['extra'].get(C.MESS_KEY_ATTACHMENTS, []):
+        for attachment in data['extra'].get(C.KEY_ATTACHMENTS, []):
             if "name" not in attachment and "url" in attachment:
                 name = (Path(unquote(urlparse(attachment['url']).path)).name
                         or C.FILE_DEFAULT_NAME)
                 attachment["name"] = name
-            if ((C.MESS_KEY_ATTACHMENTS_MEDIA_TYPE not in attachment
+            if ((C.KEY_ATTACHMENTS_MEDIA_TYPE not in attachment
                  and "name" in attachment)):
                 media_type = mimetypes.guess_type(attachment['name'], strict=False)[0]
                 if media_type:
-                    attachment[C.MESS_KEY_ATTACHMENTS_MEDIA_TYPE] = media_type
+                    attachment[C.KEY_ATTACHMENTS_MEDIA_TYPE] = media_type
 
         return data
 
--- 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)
--- a/sat/plugins/plugin_comp_ap_gateway/events.py	Thu Mar 23 15:39:48 2023 +0100
+++ b/sat/plugins/plugin_comp_ap_gateway/events.py	Thu Mar 23 15:42:21 2023 +0100
@@ -230,7 +230,7 @@
                             "value": ap_wc_value
                         })
 
-        activity = self.apg.createActivity(
+        activity = self.apg.create_activity(
             "Create" if is_new else "Update", url_actor, ap_object, activity_id=url_item
         )
         activity["@context"].append(AP_EVENTS_CONTEXT)
--- a/sat/plugins/plugin_comp_ap_gateway/http_server.py	Thu Mar 23 15:39:48 2023 +0100
+++ b/sat/plugins/plugin_comp_ap_gateway/http_server.py	Thu Mar 23 15:42:21 2023 +0100
@@ -215,7 +215,7 @@
                 raise exceptions.InternalError('"subscribed" state was expected')
             inbox = await self.apg.getAPInboxFromId(signing_actor, use_shared=False)
             actor_id = self.apg.buildAPURL(TYPE_ACTOR, ap_account)
-            accept_data = self.apg.createActivity(
+            accept_data = self.apg.create_activity(
                 "Accept", actor_id, object_=data
             )
             await self.apg.signAndPost(inbox, actor_id, accept_data)
@@ -715,7 +715,7 @@
             ordered_items = [
                 await self.apg.mb_data_2_ap_item(
                     self.apg.client,
-                    await self.apg._m.item2mbdata(
+                    await self.apg._m.item_2_mb_data(
                         self.apg.client,
                         item,
                         account_jid,
--- a/sat/plugins/plugin_comp_ap_gateway/pubsub_service.py	Thu Mar 23 15:39:48 2023 +0100
+++ b/sat/plugins/plugin_comp_ap_gateway/pubsub_service.py	Thu Mar 23 15:42:21 2023 +0100
@@ -514,7 +514,7 @@
                 requestor, service
             )
 
-            data = self.apg.createActivity("Follow", req_actor_id, recip_actor_id)
+            data = self.apg.create_activity("Follow", req_actor_id, recip_actor_id)
 
             resp = await self.apg.signAndPost(inbox, req_actor_id, data)
             if resp.code >= 300:
@@ -527,10 +527,10 @@
         req_actor_id, recip_actor_id, inbox = await self.getAPActorIdsAndInbox(
             requestor, service
         )
-        data = self.apg.createActivity(
+        data = self.apg.create_activity(
             "Undo",
             req_actor_id,
-            self.apg.createActivity(
+            self.apg.create_activity(
                 "Follow",
                 req_actor_id,
                 recip_actor_id
--- a/sat/plugins/plugin_misc_attach.py	Thu Mar 23 15:39:48 2023 +0100
+++ b/sat/plugins/plugin_misc_attach.py	Thu Mar 23 15:42:21 2023 +0100
@@ -59,6 +59,7 @@
         self.host = host
         self._u = host.plugins["UPLOAD"]
         host.trigger.add("sendMessage", self._sendMessageTrigger)
+        host.trigger.add("sendMessageComponent", self._sendMessageTrigger)
         self._attachments_handlers = {'clear': [], 'encrypted': []}
         self.register(self.defaultCanHandle, self.defaultAttach, False, -1000)
 
@@ -100,13 +101,13 @@
         """
         # we check attachment for pre-treatment like large image resizing
         # media_type will be added if missing (and if it can be guessed from path)
-        attachments = data["extra"][C.MESS_KEY_ATTACHMENTS]
+        attachments = data["extra"][C.KEY_ATTACHMENTS]
         tmp_dirs_to_clean = []
         for attachment in attachments:
-            if attachment.get(C.MESS_KEY_ATTACHMENTS_RESIZE, False):
+            if attachment.get(C.KEY_ATTACHMENTS_RESIZE, False):
                 path = Path(attachment["path"])
                 try:
-                    media_type = attachment[C.MESS_KEY_ATTACHMENTS_MEDIA_TYPE]
+                    media_type = attachment[C.KEY_ATTACHMENTS_MEDIA_TYPE]
                 except KeyError:
                     media_type = mimetypes.guess_type(path, strict=False)[0]
                     if media_type is None:
@@ -114,7 +115,7 @@
                             _("Can't resize attachment of unknown type: {attachment}")
                             .format(attachment=attachment))
                         continue
-                    attachment[C.MESS_KEY_ATTACHMENTS_MEDIA_TYPE] = media_type
+                    attachment[C.KEY_ATTACHMENTS_MEDIA_TYPE] = media_type
 
                 main_type = media_type.split('/')[0]
                 if main_type == "image":
@@ -188,6 +189,9 @@
         attachments = data["extra"]["attachments"]
 
         for attachment in attachments:
+            if "url" in attachment and not "path" in attachment:
+                log.debug(f"attachment is external, we don't upload it: {attachment}")
+                continue
             try:
                 # we pop path because we don't want it to be stored, as the file can be
                 # only in a temporary location
@@ -252,7 +256,7 @@
 
     def _sendMessageTrigger(
         self, client, mess_data, pre_xml_treatments, post_xml_treatments):
-        if mess_data['extra'].get(C.MESS_KEY_ATTACHMENTS):
+        if mess_data['extra'].get(C.KEY_ATTACHMENTS):
             post_xml_treatments.addCallback(self._attachFiles, client=client)
         return True
 
@@ -265,7 +269,7 @@
         body_elt = data["xml"].body
         if body_elt is None:
             body_elt = data["xml"].addElement("body")
-        attachments = data["extra"][C.MESS_KEY_ATTACHMENTS]
+        attachments = data["extra"][C.KEY_ATTACHMENTS]
         if attachments:
             body_links = '\n'.join(a['url'] for a in attachments)
             if str(body_elt).strip():
--- a/sat/plugins/plugin_sec_aesgcm.py	Thu Mar 23 15:39:48 2023 +0100
+++ b/sat/plugins/plugin_sec_aesgcm.py	Thu Mar 23 15:42:21 2023 +0100
@@ -156,7 +156,7 @@
         # TODO: this is to be removed when a better mechanism is available with OMEMO (now
         #   possible with the 0.4 version of OMEMO, it's possible to encrypt other stanza
         #   elements than body).
-        attachments = data["extra"][C.MESS_KEY_ATTACHMENTS]
+        attachments = data["extra"][C.KEY_ATTACHMENTS]
         if not data['message'] or data['message'] == {'': ''}:
             extra_attachments = attachments[1:]
             del attachments[1:]
@@ -165,7 +165,7 @@
             # we have a message, we must send first attachment separately
             extra_attachments = attachments[:]
             attachments.clear()
-            del data["extra"][C.MESS_KEY_ATTACHMENTS]
+            del data["extra"][C.KEY_ATTACHMENTS]
 
         body_elt = data["xml"].body
         if body_elt is None:
@@ -181,7 +181,7 @@
                 message={'': ''},
                 subject=data['subject'],
                 mess_type=data['type'],
-                extra={C.MESS_KEY_ATTACHMENTS: [attachment]},
+                extra={C.KEY_ATTACHMENTS: [attachment]},
             )
 
         if ((not data['extra']
@@ -299,7 +299,7 @@
                 else:
                     data['message'][lang] = message
                 mess_encrypted = client.encryption.isEncrypted(data)
-                attachments = data['extra'].setdefault(C.MESS_KEY_ATTACHMENTS, [])
+                attachments = data['extra'].setdefault(C.KEY_ATTACHMENTS, [])
                 for link in links:
                     path = parse.urlparse(link).path
                     attachment = {
@@ -307,7 +307,7 @@
                     }
                     media_type = mimetypes.guess_type(path, strict=False)[0]
                     if media_type is not None:
-                        attachment[C.MESS_KEY_ATTACHMENTS_MEDIA_TYPE] = media_type
+                        attachment[C.KEY_ATTACHMENTS_MEDIA_TYPE] = media_type
 
                     if mess_encrypted:
                         # we don't add the encrypted flag if the message itself is not
--- a/sat/plugins/plugin_xep_0277.py	Thu Mar 23 15:39:48 2023 +0100
+++ b/sat/plugins/plugin_xep_0277.py	Thu Mar 23 15:42:21 2023 +0100
@@ -97,7 +97,7 @@
                     "namespace": NS_ATOM,
                     "type": "blog",
                     "to_sync": True,
-                    "parser": self.item2mbdata,
+                    "parser": self.item_2_mb_data,
                     "match_cb": self._cacheNodeMatchCb,
                 }
             )
@@ -249,7 +249,7 @@
         for item in itemsEvent.items:
             if item.name == C.PS_ITEM:
                 # FIXME: service and node should be used here
-                self.item2mbdata(client, item, None, None).addCallbacks(
+                self.item_2_mb_data(client, item, None, None).addCallbacks(
                     manageItem, lambda failure: None, (C.PS_PUBLISH,)
                 )
             elif item.name == C.PS_RETRACT:
@@ -260,12 +260,12 @@
     ## data/item transformation ##
 
     @defer.inlineCallbacks
-    def item2mbdata(
+    def item_2_mb_data(
         self,
         client: SatXMPPEntity,
         item_elt: domish.Element,
         service: Optional[jid.JID],
-        # FIXME: node is Optional until all calls to item2mbdata set properly service
+        # FIXME: node is Optional until all calls to item_2_mb_data set properly service
         #   and node. Once done, the Optional must be removed here
         node: Optional[str]
     ) -> dict:
@@ -615,7 +615,7 @@
 
         defer.returnValue(microblog_data)
 
-    async def data2entry(self, client, mb_data, item_id, service, node):
+    async def mb_data_2_entry_elt(self, client, mb_data, item_id, service, node):
         """Convert a data dict to en entry usable to create an item
 
         @param mb_data: data dict as given by bridge method.
@@ -1036,7 +1036,7 @@
             await self._manageComments(client, data, service, node, item_id, access=None)
         except error.StanzaError:
             log.warning("Can't create comments node for item {}".format(item_id))
-        item = await self.data2entry(client, data, item_id, service, node)
+        item = await self.mb_data_2_entry_elt(client, data, item_id, service, node)
 
         if not await self.host.trigger.asyncPoint(
             "XEP-0277_send", client, service, node, item, data
@@ -1165,9 +1165,9 @@
         item_id = data.get("id", "")
 
         # we have to serialise then deserialise to be sure that all triggers are called
-        item_elt = await self.data2entry(client, data, item_id, service, node)
+        item_elt = await self.mb_data_2_entry_elt(client, data, item_id, service, node)
         item_elt.uri = pubsub.NS_PUBSUB
-        return await self.item2mbdata(client, item_elt, service, node)
+        return await self.item_2_mb_data(client, item_elt, service, node)
 
 
     ## retract ##
@@ -1243,7 +1243,7 @@
             extra=extra,
         )
         mb_data_list, metadata = await self._p.transItemsDataD(
-            items_data, partial(self.item2mbdata, client, service=service, node=node))
+            items_data, partial(self.item_2_mb_data, client, service=service, node=node))
         encrypted = metadata.pop("encrypted", None)
         if encrypted is not None:
             for mb_data in mb_data_list:
@@ -1461,7 +1461,7 @@
             d = self._p.transItemsDataD(
                 items_data,
                 # FIXME: service and node should be used here
-                partial(self.item2mbdata, client),
+                partial(self.item_2_mb_data, client),
                 serialise=True
             )
             d.addCallback(lambda serialised: ("", serialised))
@@ -1663,7 +1663,7 @@
                             lambda items_data: self._p.transItemsDataD(
                                 items_data,
                                 partial(
-                                    self.item2mbdata, client, service=service, node=node
+                                    self.item_2_mb_data, client, service=service, node=node
                                 ),
                                 serialise=True
                             )
@@ -1704,7 +1704,7 @@
             d.addCallback(
                 lambda items_data: self._p.transItemsDataD(
                     items_data,
-                    partial(self.item2mbdata, client, service=service, node=node),
+                    partial(self.item_2_mb_data, client, service=service, node=node),
                 )
             )
             d.addCallback(getComments)
--- a/sat/plugins/plugin_xep_0447.py	Thu Mar 23 15:39:48 2023 +0100
+++ b/sat/plugins/plugin_xep_0447.py	Thu Mar 23 15:42:21 2023 +0100
@@ -267,7 +267,7 @@
         @return: file-sharing data. It a dict whose keys correspond to
             [get_file_sharing_elt] parameters
         """
-        if file_sharing_elt.name != "file-sharing":
+        if file_sharing_elt.name != "file-sharing" or file_sharing_elt.uri != NS_SFS:
             try:
                 file_sharing_elt = next(
                     file_sharing_elt.elements(NS_SFS, "file-sharing")
@@ -295,31 +295,30 @@
             data: Dict[str, Any]
     ) -> Dict[str, Any]:
         """Check <message> for a shared file, and add it as an attachment"""
-        # XXX: XEP-0447 doesn't support several attachments in a single message, thus only
-        #   one attachment can be added
-        try:
+        # XXX: XEP-0447 doesn't support several attachments in a single message, for now
+        #   however that should be fixed in future version, and so we accept several
+        #   <file-sharing> element in a message.
+        for file_sharing_elt in message_elt.elements(NS_SFS, "file-sharing"):
             attachment = self.parse_file_sharing_elt(message_elt)
-        except exceptions.NotFound:
-            return data
 
-        if any(
-                s.get(C.MESS_KEY_ENCRYPTED, False)
-                for s in attachment["sources"]
-        ) and client.encryption.isEncrypted(data):
-            # we don't add the encrypted flag if the message itself is not encrypted,
-            # because the decryption key is part of the link, so sending it over
-            # unencrypted channel is like having no encryption at all.
-            attachment[C.MESS_KEY_ENCRYPTED] = True
+            if any(
+                    s.get(C.MESS_KEY_ENCRYPTED, False)
+                    for s in attachment["sources"]
+            ) and client.encryption.isEncrypted(data):
+                # we don't add the encrypted flag if the message itself is not encrypted,
+                # because the decryption key is part of the link, so sending it over
+                # unencrypted channel is like having no encryption at all.
+                attachment[C.MESS_KEY_ENCRYPTED] = True
 
-        attachments = data['extra'].setdefault(C.MESS_KEY_ATTACHMENTS, [])
-        attachments.append(attachment)
+            attachments = data['extra'].setdefault(C.KEY_ATTACHMENTS, [])
+            attachments.append(attachment)
 
         return data
 
     async def attach(self, client, data):
         # XXX: for now, XEP-0447 only allow to send one file per <message/>, thus we need
         #   to send each file in a separate message
-        attachments = data["extra"][C.MESS_KEY_ATTACHMENTS]
+        attachments = data["extra"][C.KEY_ATTACHMENTS]
         if not data['message'] or data['message'] == {'': ''}:
             extra_attachments = attachments[1:]
             del attachments[1:]
@@ -327,7 +326,7 @@
             # we have a message, we must send first attachment separately
             extra_attachments = attachments[:]
             attachments.clear()
-            del data["extra"][C.MESS_KEY_ATTACHMENTS]
+            del data["extra"][C.KEY_ATTACHMENTS]
 
         if attachments:
             if len(attachments) > 1:
@@ -343,8 +342,10 @@
                     file_hash = None
                 file_sharing_elt = self.get_file_sharing_elt(
                     [{"url": attachment["url"]}],
-                    name=attachment["name"],
-                    size=attachment["size"],
+                    name=attachment.get("name"),
+                    size=attachment.get("size"),
+                    desc=attachment.get("desc"),
+                    media_type=attachment.get("media_type"),
                     file_hash=file_hash
                 )
                 data["xml"].addChild(file_sharing_elt)
@@ -356,7 +357,7 @@
                 message={'': ''},
                 subject=data['subject'],
                 mess_type=data['type'],
-                extra={C.MESS_KEY_ATTACHMENTS: [attachment]},
+                extra={C.KEY_ATTACHMENTS: [attachment]},
             )
 
         if ((not data['extra']
--- a/sat/plugins/plugin_xep_0448.py	Thu Mar 23 15:39:48 2023 +0100
+++ b/sat/plugins/plugin_xep_0448.py	Thu Mar 23 15:42:21 2023 +0100
@@ -263,7 +263,7 @@
         # XXX: for now, XEP-0447/XEP-0448 only allow to send one file per <message/>, thus
         #   we need to send each file in a separate message, in the same way as for
         #   plugin_sec_aesgcm.
-        attachments = data["extra"][C.MESS_KEY_ATTACHMENTS]
+        attachments = data["extra"][C.KEY_ATTACHMENTS]
         if not data['message'] or data['message'] == {'': ''}:
             extra_attachments = attachments[1:]
             del attachments[1:]
@@ -271,7 +271,7 @@
             # we have a message, we must send first attachment separately
             extra_attachments = attachments[:]
             attachments.clear()
-            del data["extra"][C.MESS_KEY_ATTACHMENTS]
+            del data["extra"][C.KEY_ATTACHMENTS]
 
         if attachments:
             if len(attachments) > 1:
@@ -319,7 +319,7 @@
                 message={'': ''},
                 subject=data['subject'],
                 mess_type=data['type'],
-                extra={C.MESS_KEY_ATTACHMENTS: [attachment]},
+                extra={C.KEY_ATTACHMENTS: [attachment]},
             )
 
         if ((not data['extra']
--- a/sat_frontends/jp/cmd_message.py	Thu Mar 23 15:39:48 2023 +0100
+++ b/sat_frontends/jp/cmd_message.py	Thu Mar 23 15:42:21 2023 +0100
@@ -145,7 +145,7 @@
             to_send.append(msg)
 
         if self.args.attachments:
-            attachments = extra[C.MESS_KEY_ATTACHMENTS] = []
+            attachments = extra[C.KEY_ATTACHMENTS] = []
             for attachment in self.args.attachments:
                 try:
                     file_path = str(Path(attachment).resolve(strict=True))
@@ -155,10 +155,10 @@
                     attachments.append({"path": file_path})
 
         for idx, msg in enumerate(to_send):
-            if idx > 0 and C.MESS_KEY_ATTACHMENTS in extra:
+            if idx > 0 and C.KEY_ATTACHMENTS in extra:
                 # if we send several messages, we only want to send attachments with the
                 # first one
-                del extra[C.MESS_KEY_ATTACHMENTS]
+                del extra[C.KEY_ATTACHMENTS]
             try:
                 await self.host.bridge.messageSend(
                     dest_jid,
--- a/sat_frontends/quick_frontend/quick_chat.py	Thu Mar 23 15:39:48 2023 +0100
+++ b/sat_frontends/quick_frontend/quick_chat.py	Thu Mar 23 15:42:21 2023 +0100
@@ -223,7 +223,7 @@
 
     @property
     def attachments(self):
-        return self.extra.get(C.MESS_KEY_ATTACHMENTS)
+        return self.extra.get(C.KEY_ATTACHMENTS)
 
 
 class MessageWidget:
@@ -733,7 +733,7 @@
             )
             return
 
-        if ((not msg and not subject and not extra[C.MESS_KEY_ATTACHMENTS]
+        if ((not msg and not subject and not extra[C.KEY_ATTACHMENTS]
              and type_ != C.MESS_TYPE_INFO)):
             log.warning("Received an empty message for uid {}".format(uid))
             return