Repository: hikari-py/hikari
Pull request #2336 (2025-04-28 08:48:21)

Fix duplicated uploaded attachments when reusing resources in embeds
### Summary
<!-- Small summary of the merge request -->

### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [x] I have run `nox` and all the pipelines have passed.
- [x] I have made unittests according to the code I have added/modified/deleted.

### Related issues
<!--
To mention an issue use `#issue-id` and to mention a merge request use `!merge-request-id`
To close/fix an issue use `Close #issue-id` or `Fix #issue-id` (depending on the merge request)
-->


review-ai-agent[bot]: <h3>Pull Request Revisions</h3>
<table><tr><th>Revision</th><th>Description</th></tr><tr><td><a href="https://review.ai/pr/hikari-py/hikari/2336?rev=1">r1</a></td><td><i>Generating summary...</i></td></tr></table><hr/>🔍 AI review in progress for <a href="https://review.ai/pr/hikari-py/hikari/2336?rev=1">r1</a><details><summary>Help</summary>
React with emojis to give feedback on AI-generated reviews:
<ul>
<li>👍 means the feedback was helpful and actionable</li>
<li>👎 means the feedback was incorrect or unhelpful</li>
</ul>
💬 Replying to feedback with a comment helps us improve the system. Your input also contributes to shaping future interactions with the AI reviewer.
<br/><br/>
We'd love to hear from you—reach out anytime at <a href="mailto:team@review.ai">team@review.ai</a>.
</details>

Patch:
diff --git a/changes/2335.bugfix.md b/changes/2335.bugfix.md
new file mode 100644
index 00000000..212cb85a
--- /dev/null
+++ b/changes/2335.bugfix.md
@@ -0,0 +1,1 @@
+Fix event filtering for interaction create specialized events
diff --git a/changes/2336.bugfix.md b/changes/2336.bugfix.md
new file mode 100644
index 00000000..9c0af97e
--- /dev/null
+++ b/changes/2336.bugfix.md
@@ -0,0 +1,1 @@
+Fix duplicated uploaded attachments when reusing resources in embeds
diff --git a/changes/2336.optimization.md b/changes/2336.optimization.md
new file mode 100644
index 00000000..76fdcc5b
--- /dev/null
+++ b/changes/2336.optimization.md
@@ -0,0 +1,1 @@
+Optimize reusing file resources when uploading repeated ones
diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py
index 4ea92165..00c9958b 100644
--- a/hikari/impl/event_manager.py
+++ b/hikari/impl/event_manager.py
@@ -840,7 +840,14 @@ async def on_webhooks_update(self, shard: gateway_shard.GatewayShard, payload: d
         """See https://discord.com/developers/docs/topics/gateway-events#webhooks-update for more info."""
         await self.dispatch(self._event_factory.deserialize_webhook_update_event(shard, payload))
 
-    @event_manager_base.filtered(interaction_events.InteractionCreateEvent)
+    @event_manager_base.filtered(
+        (
+            interaction_events.CommandInteractionCreateEvent,
+            interaction_events.ComponentInteractionCreateEvent,
+            interaction_events.AutocompleteInteractionCreateEvent,
+            interaction_events.ModalInteractionCreateEvent,
+        )
+    )
     async def on_interaction_create(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway-events#interaction-create for more info."""
         await self.dispatch(self._event_factory.deserialize_interaction_create_event(shard, payload))
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index cda45803..733b0205 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -1494,6 +1494,7 @@ def _build_message_payload(  # noqa: C901, PLR0912, PLR0915
             attachment = content
             content = undefined.UNDEFINED
 
+        resources: list[files.Resource[typing.Any]] = []
         final_attachments: list[files.Resourceish | messages_.Attachment] = []
         if attachment:
             final_attachments.append(attachment)
@@ -1505,7 +1506,7 @@ def _build_message_payload(  # noqa: C901, PLR0912, PLR0915
             if component is not None:
                 component_payload, component_attachments = component.build()
                 serialized_components = [component_payload]
-                final_attachments.extend(component_attachments)
+                resources.extend(component_attachments)
 
                 if component.type in _V2_COMPONENT_TYPES:
                     if flags is undefined.UNDEFINED:
@@ -1520,7 +1521,7 @@ def _build_message_payload(  # noqa: C901, PLR0912, PLR0915
                 for comp in components:
                     component_payload, component_attachments = comp.build()
                     serialized_components.append(component_payload)
-                    final_attachments.extend(component_attachments)
+                    resources.extend(component_attachments)
 
                     if comp.type in _V2_COMPONENT_TYPES:
                         if flags is undefined.UNDEFINED:
@@ -1531,7 +1532,7 @@ def _build_message_payload(  # noqa: C901, PLR0912, PLR0915
         if embed is not undefined.UNDEFINED:
             if embed is not None:
                 embed_payload, embed_attachments = self._entity_factory.serialize_embed(embed)
-                final_attachments.extend(embed_attachments)
+                resources.extend(embed_attachments)
                 serialized_embeds = [embed_payload]
 
             else:
@@ -1542,7 +1543,7 @@ def _build_message_payload(  # noqa: C901, PLR0912, PLR0915
             if embeds is not None:
                 for e in embeds:
                     embed_payload, embed_attachments = self._entity_factory.serialize_embed(e)
-                    final_attachments.extend(embed_attachments)
+                    resources.extend(embed_attachments)
                     serialized_embeds.append(embed_payload)
 
         body = data_binding.JSONObjectBuilder()
@@ -1562,10 +1563,16 @@ def _build_message_payload(  # noqa: C901, PLR0912, PLR0915
             )
 
         form_builder: data_binding.URLEncodedFormBuilder | None = None
-        if final_attachments:
+        if resources or final_attachments:
             attachments_payload = []
             attachment_id = 0
 
+            # The rationale behind this large (and probably confusing) piece of code
+            # is to always upload all attachments specified as `attachments=[]`, no
+            # matter if they are the same, but for other resources spread across
+            # all the other components/embeds, deduplicate them and only upload them once.
+            final_attachments.extend(list(dict.fromkeys(resources)))
+
             for f in final_attachments:
                 if edit and isinstance(f, messages_.Attachment):
                     attachments_payload.append({"id": f.id, "filename": f.filename})


Test patch:
diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py
index 62034d02..f2f7b5fe 100644
--- a/tests/hikari/impl/test_rest.py
+++ b/tests/hikari/impl/test_rest.py
@@ -1547,6 +1547,109 @@ def test__build_message_payload_with_edit_and_attachment_object_passed(self, res
             ]
         )
 
+    def test__build_message_payload_with_duplicated_resources(self, rest_client):
+        attachment1 = mock.Mock()
+        attachment2 = mock.Mock(message_models.Attachment, id=123, filename="attachment123.png")
+        component_attachment = mock.Mock(filename="component.png")
+        component_attachment2 = mock.Mock(filename="component2.png")
+        component1 = mock.Mock(
+            build=mock.Mock(return_value=({"component": 1}, (component_attachment, component_attachment)))
+        )
+        component2 = mock.Mock(
+            build=mock.Mock(return_value=({"component": 2}, (component_attachment2, component_attachment2)))
+        )
+        embed1 = mock.Mock()
+        embed2 = mock.Mock()
+        embed_attachment1 = mock.Mock()
+        embed_attachment2 = mock.Mock()
+        resource_attachment1 = mock.Mock(filename="attachment.png")
+        resource_attachment2 = mock.Mock(filename="attachment2.png")
+        resource_attachment3 = mock.Mock(filename="attachment3.png")
+        resource_attachment4 = mock.Mock(filename="attachment4.png")
+        resource_attachment5 = mock.Mock(filename="attachment5.png")
+        resource_attachment6 = mock.Mock(filename="attachment6.png")
+
+        rest_client._entity_factory.serialize_embed.side_effect = [
+            ({"embed": 1}, [embed_attachment1, embed_attachment2]),
+            ({"embed": 2}, [embed_attachment2, embed_attachment1]),
+        ]
+
+        with (
+            mock.patch.object(
+                files,
+                "ensure_resource",
+                side_effect=[
+                    resource_attachment1,
+                    resource_attachment2,
+                    resource_attachment3,
+                    resource_attachment4,
+                    resource_attachment5,
+                    resource_attachment6,
+                ],
+            ) as ensure_resource,
+            mock.patch.object(data_binding, "URLEncodedFormBuilder") as url_encoded_form,
+        ):
+            body, form = rest_client._build_message_payload(
+                content=987654321,
+                attachments=[attachment1, attachment1, attachment2],
+                components=[component1, component2],
+                embeds=[embed1, embed2],
+                flags=120,
+                tts=True,
+                mentions_everyone=None,
+                mentions_reply=None,
+                user_mentions=None,
+                role_mentions=None,
+                edit=True,
+            )
+
+        # Returned
+        assert body == {
+            "content": "987654321",
+            "tts": True,
+            "flags": 120,
+            "embeds": [{"embed": 1}, {"embed": 2}],
+            "components": [{"component": 1}, {"component": 2}],
+            "attachments": [
+                {"id": 0, "filename": "attachment.png"},
+                {"id": 1, "filename": "attachment2.png"},
+                {"id": 123, "filename": "attachment123.png"},
+                {"id": 2, "filename": "attachment3.png"},
+                {"id": 3, "filename": "attachment4.png"},
+                {"id": 4, "filename": "attachment5.png"},
+                {"id": 5, "filename": "attachment6.png"},
+            ],
+            "allowed_mentions": {"parse": []},
+        }
+        assert form is url_encoded_form.return_value
+
+        # Attachments
+        assert ensure_resource.call_count == 6
+        ensure_resource.assert_has_calls(
+            [
+                mock.call(attachment1),
+                mock.call(attachment1),
+                mock.call(component_attachment),
+                mock.call(component_attachment2),
+                mock.call(embed_attachment1),
+                mock.call(embed_attachment2),
+            ]
+        )
+
+        # Form builder
+        url_encoded_form.assert_called_once_with()
+        assert url_encoded_form.return_value.add_resource.call_count == 6
+        url_encoded_form.return_value.add_resource.assert_has_calls(
+            [
+                mock.call("files[0]", resource_attachment1),
+                mock.call("files[1]", resource_attachment2),
+                mock.call("files[2]", resource_attachment3),
+                mock.call("files[3]", resource_attachment4),
+                mock.call("files[4]", resource_attachment5),
+                mock.call("files[5]", resource_attachment6),
+            ]
+        )
+
     @pytest.mark.parametrize(
         ("singular_arg", "plural_arg"),
         [("attachment", "attachments"), ("component", "components"), ("embed", "embeds"), ("sticker", "stickers")],