Repository: hikari-py/hikari
Pull request #1367 (2022-11-20 14:33:11)

Only use attachment upload method when editing messages
### Summary
I initially tried to bound the attachments to a message_id or an interaction ID to ensure that the attachments were (at least at some point) part of that message we are editing. This unfortunately was stopped by discord, as all the interactions use the application_id, not the interaction id :(

So this will have to work for now

### 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.



Patch:
diff --git a/changes/1367.bugfix.md b/changes/1367.bugfix.md
new file mode 100644
index 00000000..1528e6db
--- /dev/null
+++ b/changes/1367.bugfix.md
@@ -0,0 +1,1 @@
+Allow re-uploading attachments when creating messages
diff --git a/dev-requirements/flake8.txt b/dev-requirements/flake8.txt
index 15fef9f8..8a93b9a0 100644
--- a/dev-requirements/flake8.txt
+++ b/dev-requirements/flake8.txt
@@ -4,7 +4,7 @@ flake8==5.0.4
 # Ref: https://github.com/DmytroLitvinov/awesome-flake8-extensions
 
 flake8-bandit~=4.1.1                    # runs bandit
-flake8-black==0.3.4                     # runs black
+flake8-black==0.3.5                     # runs black
 flake8-broken-line==0.6.0               # forbey "\" linebreaks
 flake8-builtins==2.0.1                  # builtin shadowing checks
 flake8-coding==1.3.2                    # coding magic-comment detection
@@ -14,7 +14,7 @@ flake8-executable==2.1.2                # shebangs
 flake8-fixme==1.1.1                     # "fix me" counter
 flake8-functions==0.0.7                 # function linting
 flake8-html==0.4.2                      # html output
-flake8-isort==5.0.0                     # runs isort
+flake8-isort==5.0.3                     # runs isort
 flake8-mutable==1.2.0                   # mutable default argument detection
 flake8-pep3101==2.0.0                   # new-style format strings only
 flake8-print==5.0.0                     # complain about print statements in code
diff --git a/dev-requirements/nox.txt b/dev-requirements/nox.txt
index 19ac118c..749e4f01 100644
--- a/dev-requirements/nox.txt
+++ b/dev-requirements/nox.txt
@@ -1,1 +1,1 @@
-nox==2022.8.7
+nox==2022.11.21
diff --git a/dev-requirements/pyright.txt b/dev-requirements/pyright.txt
index c3f7fb20..9d3c5ee6 100644
--- a/dev-requirements/pyright.txt
+++ b/dev-requirements/pyright.txt
@@ -1,1 +1,1 @@
-pyright==1.1.280
+pyright==1.1.281
diff --git a/dev-requirements/slotscheck.txt b/dev-requirements/slotscheck.txt
index aca35f68..89c1338f 100644
--- a/dev-requirements/slotscheck.txt
+++ b/dev-requirements/slotscheck.txt
@@ -1,1 +1,1 @@
-slotscheck==0.16.0
+slotscheck==0.16.1
diff --git a/hikari/impl/__init__.py b/hikari/impl/__init__.py
index 3bafe352..df328fbf 100644
--- a/hikari/impl/__init__.py
+++ b/hikari/impl/__init__.py
@@ -41,5 +41,6 @@
 from hikari.impl.rate_limits import *
 from hikari.impl.rest import *
 from hikari.impl.rest_bot import *
+from hikari.impl.shard import *
 from hikari.impl.special_endpoints import *
 from hikari.impl.voice import *
diff --git a/hikari/impl/__init__.pyi b/hikari/impl/__init__.pyi
index 04c7daab..1cfdb5ed 100644
--- a/hikari/impl/__init__.pyi
+++ b/hikari/impl/__init__.pyi
@@ -12,5 +12,6 @@ from hikari.impl.interaction_server import *
 from hikari.impl.rate_limits import *
 from hikari.impl.rest import *
 from hikari.impl.rest_bot import *
+from hikari.impl.shard import *
 from hikari.impl.special_endpoints import *
 from hikari.impl.voice import *
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index feecdea8..2c14f712 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -1327,7 +1327,7 @@ def _build_message_payload(  # noqa: C901- Function too complex
             attachment_id = 0
 
             for f in final_attachments:
-                if isinstance(f, messages_.Attachment):
+                if edit and isinstance(f, messages_.Attachment):
                     attachments_payload.append({"id": f.id, "filename": f.filename})
                     continue
 
diff --git a/hikari/impl/shard.py b/hikari/impl/shard.py
index ac09a42c..9ace9dad 100644
--- a/hikari/impl/shard.py
+++ b/hikari/impl/shard.py
@@ -345,7 +345,6 @@ def _serialize_activity(activity: typing.Optional[presences.Activity]) -> data_b
     return {"name": activity.name, "type": int(activity.type), "url": activity.url}
 
 
-@typing.final
 class GatewayShardImpl(shard.GatewayShard):
     """Implementation of a V10 compatible gateway.
 
diff --git a/hikari/messages.py b/hikari/messages.py
index 2dbc1569..22355725 100644
--- a/hikari/messages.py
+++ b/hikari/messages.py
@@ -201,6 +201,8 @@ class Attachment(snowflakes.Unique, files.WebResource):
 
     You can use this object in the same way as a `hikari.files.WebResource`,
     by passing it as an attached file when creating a message, etc.
+
+    It can also be used when editing a message to keep a previous attachment.
     """
 
     id: snowflakes.Snowflake = attr.field(hash=True, repr=True)


Test patch:
diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py
index 8184f9eb..b013b522 100644
--- a/tests/hikari/impl/test_rest.py
+++ b/tests/hikari/impl/test_rest.py
@@ -1381,6 +1381,7 @@ def test__build_message_payload_with_plural_args(self, rest_client):
         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")
         component1 = mock.Mock(build=mock.Mock(return_value={"component": 1}))
         component2 = mock.Mock(build=mock.Mock(return_value={"component": 2}))
         embed1 = object()
@@ -1405,6 +1406,7 @@ def test__build_message_payload_with_plural_args(self, rest_client):
                     resource_attachment3,
                     resource_attachment4,
                     resource_attachment5,
+                    resource_attachment6,
                 ],
             )
         )
@@ -1440,21 +1442,22 @@ def test__build_message_payload_with_plural_args(self, rest_client):
             "components": [{"component": 1}, {"component": 2}],
             "attachments": [
                 {"id": 0, "filename": "attachment.png"},
-                {"id": 123, "filename": "attachment123.png"},
                 {"id": 1, "filename": "attachment2.png"},
                 {"id": 2, "filename": "attachment3.png"},
                 {"id": 3, "filename": "attachment4.png"},
                 {"id": 4, "filename": "attachment5.png"},
+                {"id": 5, "filename": "attachment6.png"},
             ],
             "allowed_mentions": {"allowed_mentions": 1},
         }
         assert form is url_encoded_form.return_value
 
         # Attachments
-        assert ensure_resource.call_count == 5
+        assert ensure_resource.call_count == 6
         ensure_resource.assert_has_calls(
             [
                 mock.call(attachment1),
+                mock.call(attachment2),
                 mock.call(embed_attachment1),
                 mock.call(embed_attachment2),
                 mock.call(embed_attachment3),
@@ -1475,6 +1478,103 @@ def test__build_message_payload_with_plural_args(self, rest_client):
             mentions_everyone, mentions_reply, user_mentions, role_mentions
         )
 
+        # Form builder
+        url_encoded_form.assert_called_once_with(executor=rest_client._executor)
+        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),
+            ]
+        )
+
+    def test__build_message_payload_with_edit_and_attachment_object_passed(self, rest_client):
+        attachment1 = object()
+        attachment2 = mock.Mock(message_models.Attachment, id=123, filename="attachment123.png")
+        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")
+        component1 = mock.Mock(build=mock.Mock(return_value={"component": 1}))
+        component2 = mock.Mock(build=mock.Mock(return_value={"component": 2}))
+        embed1 = object()
+        embed2 = object()
+        embed_attachment1 = object()
+        embed_attachment2 = object()
+        embed_attachment3 = object()
+        embed_attachment4 = object()
+
+        stack = contextlib.ExitStack()
+        ensure_resource = stack.enter_context(
+            mock.patch.object(
+                files,
+                "ensure_resource",
+                side_effect=[
+                    resource_attachment1,
+                    resource_attachment2,
+                    resource_attachment3,
+                    resource_attachment4,
+                    resource_attachment5,
+                ],
+            )
+        )
+        url_encoded_form = stack.enter_context(mock.patch.object(data_binding, "URLEncodedFormBuilder"))
+        rest_client._entity_factory.serialize_embed.side_effect = [
+            ({"embed": 1}, [embed_attachment1, embed_attachment2]),
+            ({"embed": 2}, [embed_attachment3, embed_attachment4]),
+        ]
+
+        with stack:
+            body, form = rest_client._build_message_payload(
+                content=987654321,
+                attachments=[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": 123, "filename": "attachment123.png"},
+                {"id": 1, "filename": "attachment2.png"},
+                {"id": 2, "filename": "attachment3.png"},
+                {"id": 3, "filename": "attachment4.png"},
+                {"id": 4, "filename": "attachment5.png"},
+            ],
+            "allowed_mentions": {"parse": []},
+        }
+        assert form is url_encoded_form.return_value
+
+        # Attachments
+        assert ensure_resource.call_count == 5
+        ensure_resource.assert_has_calls(
+            [
+                mock.call(attachment1),
+                mock.call(embed_attachment1),
+                mock.call(embed_attachment2),
+                mock.call(embed_attachment3),
+                mock.call(embed_attachment4),
+            ]
+        )
+
         # Form builder
         url_encoded_form.assert_called_once_with(executor=rest_client._executor)
         assert url_encoded_form.return_value.add_resource.call_count == 5