Repository: hikari-py/hikari (python)

Issue:
Add missing `old_x` fields in events
There is currently a bit of inconsistency in the `old_x` fields in the update and delete events for objects we cache

Examples that I have seen are (there might be more):
- *MessageDeleteEvent
- ~~*ChannelDeleteEvent (no need, discord send the info for this one)~~
- GuildLeaveEvent (solved by #806)


Pull request description:
Split bulk message delete and message delete into separate events
### 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
Closes #766


Patch:
diff --git a/changes/897.breaking.md b/changes/897.breaking.md
new file mode 100644
index 00000000..76259a50
--- /dev/null
+++ b/changes/897.breaking.md
@@ -0,0 +1,2 @@
+Split bulk message delete from normal delete
+  - The new event is now `hikari.events.message_events.GuildBulkMessageDeleteEvent`
diff --git a/changes/897.feature.md b/changes/897.feature.md
new file mode 100644
index 00000000..00052b25
--- /dev/null
+++ b/changes/897.feature.md
@@ -0,0 +1,1 @@
+Add `old_message` attribute to `hikari.events.message_events.MessageDelete`
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 14a5baed..593fedb6 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -9,13 +9,13 @@ mock==4.0.3
 pytest==6.2.5
 pytest-asyncio==0.16.0
 pytest-cov==3.0.0
-pytest-randomly==3.10.1
+pytest-randomly==3.10.2
 
 # Coverage testing.
-coverage==6.1.1
+coverage==6.1.2
 
 # Other stuff
-async-timeout==4.0.0  # Used for timeouts in some test cases.
+async-timeout==4.0.1  # Used for timeouts in some test cases.
 
 #################
 # DOCUMENTATION #
diff --git a/hikari/api/event_factory.py b/hikari/api/event_factory.py
index 8f108055..3a58e196 100644
--- a/hikari/api/event_factory.py
+++ b/hikari/api/event_factory.py
@@ -36,6 +36,7 @@
     from hikari import invites as invite_models
     from hikari import messages as messages_models
     from hikari import presences as presences_models
+    from hikari import snowflakes
     from hikari import users as user_models
     from hikari import voices as voices_models
     from hikari.api import shard as gateway_shard
@@ -752,7 +753,11 @@ def deserialize_message_update_event(
 
     @abc.abstractmethod
     def deserialize_message_delete_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_message: typing.Optional[messages_models.Message],
     ) -> message_events.MessageDeleteEvent:
         """Parse a raw payload from Discord into a message delete event object.
 
@@ -762,6 +767,8 @@ def deserialize_message_delete_event(
             The shard that emitted this event.
         payload : hikari.internal.data_binding.JSONObject
             The dict payload to parse.
+        old_message : typing.Optional[hikari.messages.Message]
+            The old message object.
 
         Returns
         -------
@@ -770,10 +777,14 @@ def deserialize_message_delete_event(
         """
 
     @abc.abstractmethod
-    def deserialize_message_delete_bulk_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
-    ) -> message_events.MessageDeleteEvent:
-        """Parse a raw payload from Discord into a message delete bulk event object.
+    def deserialize_guild_message_delete_bulk_event(
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_messages: typing.Mapping[snowflakes.Snowflake, messages_models.Message],
+    ) -> message_events.GuildBulkMessageDeleteEvent:
+        """Parse a raw payload from Discord into a guild message delete bulk event object.
 
         Parameters
         ----------
@@ -781,16 +792,13 @@ def deserialize_message_delete_bulk_event(
             The shard that emitted this event.
         payload : hikari.internal.data_binding.JSONObject
             The dict payload to parse.
+        old_messages : typing.Mapping[hikari.snowflakes.Snowflake, hikari.messages.Message]
+            A mapping of the old message objects.
 
         Returns
         -------
-        hikari.events.message_events.MessageDeleteEvent
-            The parsed message delete bulk event object.
-
-        Raises
-        ------
-        builtins.NotImplementedError
-            If a bulk delete occurs in a DM channel.
+        hikari.events.message_events.GuildBulkMessageDeleteEvent
+            The parsed guild message delete bulk event object.
         """
 
     ###################
diff --git a/hikari/events/message_events.py b/hikari/events/message_events.py
index 67f50c52..31c23186 100644
--- a/hikari/events/message_events.py
+++ b/hikari/events/message_events.py
@@ -32,6 +32,7 @@
     "GuildMessageCreateEvent",
     "GuildMessageUpdateEvent",
     "GuildMessageDeleteEvent",
+    "GuildBulkMessageDeleteEvent",
     "DMMessageCreateEvent",
     "DMMessageUpdateEvent",
     "DMMessageDeleteEvent",
@@ -594,57 +595,26 @@ class DMMessageUpdateEvent(MessageUpdateEvent):
 
 @base_events.requires_intents(intents.Intents.GUILD_MESSAGES, intents.Intents.DM_MESSAGES)
 class MessageDeleteEvent(MessageEvent, abc.ABC):
-    """Special event that is triggered when one or more messages get deleted.
+    """Special event that is triggered when a message gets deleted.
 
     !!! note
         Due to Discord limitations, most message information is unavailable
         during deletion events.
-
-    You can check if the message was in a singular deletion by checking the
-    `is_bulk` attribute.
     """
 
     __slots__: typing.Sequence[str] = ()
 
-    @property
-    def message_id(self) -> snowflakes.Snowflake:
-        """Get the ID of the first deleted message.
-
-        This is contextually useful if you know this is not a bulk deletion
-        event. For all other purposes, this is the same as running
-        `next(iter(event.message_ids))`.
-
-        Returns
-        -------
-        hikari.snowflakes.Snowflake
-            The first deleted message ID.
-        """
-        try:
-            return next(iter(self.message_ids))
-        except StopIteration:
-            raise RuntimeError("No messages were sent in a bulk delete! Please shout at Discord to fix this!") from None
-
     @property
     @abc.abstractmethod
-    def message_ids(self) -> typing.AbstractSet[snowflakes.Snowflake]:
-        """Set of message IDs that were bulk deleted.
-
-        Returns
-        -------
-        typing.AbstractSet[hikari.snowflakes.Snowflake]
-            A sequence of message IDs that were bulk deleted.
-        """
+    def message_id(self) -> snowflakes.Snowflake:
+        """ID of the message that was deleted."""
 
     @property
     @abc.abstractmethod
-    def is_bulk(self) -> bool:
-        """Flag that determines whether this was a bulk deletion or not.
+    def old_message(self) -> typing.Optional[messages.Message]:
+        """Object of the message that was deleted.
 
-        Returns
-        -------
-        builtins.bool
-            `builtins.True` if this was a bulk deletion, or `builtins.False`
-            if it was a regular message deletion.
+        Will be `None` if the message was not found in the cache.
         """
 
 
@@ -652,15 +622,11 @@ def is_bulk(self) -> bool:
 @attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.GUILD_MESSAGES)
 class GuildMessageDeleteEvent(MessageDeleteEvent):
-    """Event that is triggered if messages are deleted in a guild.
+    """Event that is triggered if a message is deleted in a guild.
 
     !!! note
         Due to Discord limitations, most message information is unavailable
         during deletion events.
-
-    This is triggered for singular message deletion, and bulk message
-    deletion. You can check if the message was in a singular deletion by
-    checking the `is_bulk` attribute.
     """
 
     app: traits.RESTAware = attr.field(metadata={attr_extensions.SKIP_DEEP_COPY: True})
@@ -670,25 +636,19 @@ class GuildMessageDeleteEvent(MessageDeleteEvent):
     # <<inherited docstring from MessageEvent>>
 
     guild_id: snowflakes.Snowflake = attr.field()
-    """ID of the guild that this event occurred in.
+    """ID of the guild that this event occurred in."""
 
-    Returns
-    -------
-    hikari.snowflakes.Snowflake
-        The ID of the guild.
-    """
-
-    is_bulk: bool = attr.field()
+    message_id: snowflakes.Snowflake = attr.field()
     # <<inherited docstring from MessageDeleteEvent>>
 
-    message_ids: typing.AbstractSet[snowflakes.Snowflake] = attr.field()
+    old_message: typing.Optional[messages.Message] = attr.field()
     # <<inherited docstring from MessageDeleteEvent>>
 
     shard: shard_.GatewayShard = attr.field(metadata={attr_extensions.SKIP_DEEP_COPY: True})
     # <<inherited docstring from ShardEvent>>
 
     def get_channel(self) -> typing.Optional[channels.TextableGuildChannel]:
-        """Get the cached channel the messages were sent in, if known.
+        """Get the cached channel the message were sent in, if known.
 
         Returns
         -------
@@ -728,17 +688,11 @@ def get_guild(self) -> typing.Optional[guilds.GatewayGuild]:
 @attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.DM_MESSAGES)
 class DMMessageDeleteEvent(MessageDeleteEvent):
-    """Event that is triggered if messages are deleted in a DM.
+    """Event that is triggered if a message is deleted in a DM.
 
     !!! note
         Due to Discord limitations, most message information is unavailable
         during deletion events.
-
-    This is triggered for singular message deletion, and bulk message
-    deletion, although the latter is not expected to occur in DMs.
-
-    You can check if the message was in a singular deletion by checking the
-    `is_bulk` attribute.
     """
 
     app: traits.RESTAware = attr.field(metadata={attr_extensions.SKIP_DEEP_COPY: True})
@@ -747,11 +701,80 @@ class DMMessageDeleteEvent(MessageDeleteEvent):
     channel_id: snowflakes.Snowflake = attr.field()
     # <<inherited docstring from MessageEvent>>
 
-    is_bulk: bool = attr.field()
+    message_id: snowflakes.Snowflake = attr.field()
     # <<inherited docstring from MessageDeleteEvent>>
 
-    message_ids: typing.AbstractSet[snowflakes.Snowflake] = attr.field()
+    old_message: typing.Optional[messages.Message] = attr.field()
     # <<inherited docstring from MessageDeleteEvent>>
 
     shard: shard_.GatewayShard = attr.field(metadata={attr_extensions.SKIP_DEEP_COPY: True})
     # <<inherited docstring from ShardEvent>>
+
+
+@attr_extensions.with_copy
+@attr.define(kw_only=True, weakref_slot=False)
+@base_events.requires_intents(intents.Intents.GUILD_MESSAGES)
+class GuildBulkMessageDeleteEvent(shard_events.ShardEvent):
+    """Event that is triggered when a bulk deletion is triggered in a guild.
+
+    !!! note
+        Due to Discord limitations, most message information is unavailable
+        during deletion events.
+    """
+
+    app: traits.RESTAware = attr.field(metadata={attr_extensions.SKIP_DEEP_COPY: True})
+    # <<inherited docstring from Event>>
+
+    channel_id: snowflakes.Snowflake = attr.field()
+    """ID of the channel that this event concerns."""
+
+    guild_id: snowflakes.Snowflake = attr.field()
+    """ID of the guild that this event occurred in."""
+
+    message_ids: typing.AbstractSet[snowflakes.Snowflake] = attr.field()
+    """Set of message IDs that were bulk deleted."""
+
+    old_messages: typing.Mapping[snowflakes.Snowflake, messages.Message] = attr.field()
+    """Mapping of a snowflake to the deleted message object.
+
+    If the message was not found in the cache it will be missing from the mapping.
+    """
+
+    shard: shard_.GatewayShard = attr.field(metadata={attr_extensions.SKIP_DEEP_COPY: True})
+    # <<inherited docstring from ShardEvent>>
+
+    def get_channel(self) -> typing.Optional[channels.TextableGuildChannel]:
+        """Get the cached channel the messages were sent in, if known.
+
+        Returns
+        -------
+        typing.Optional[hikari.channels.TextableGuildChannel]
+            The channel the messages were sent in, or `builtins.None` if not
+            known/cached.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return None
+
+        channel = self.app.cache.get_guild_channel(self.channel_id)
+        assert channel is None or isinstance(
+            channel, channels.TextableGuildChannel
+        ), f"Cached channel ID is not a TextableGuildChannel, but a {type(channel).__name__}!"
+        return channel
+
+    def get_guild(self) -> typing.Optional[guilds.GatewayGuild]:
+        """Get the cached guild this event corresponds to, if known.
+
+        !!! note
+            You will need `hikari.intents.Intents.GUILDS` enabled to receive this
+            information.
+
+        Returns
+        -------
+        hikari.guilds.GatewayGuild
+            The gateway guild that this event corresponds to, if known and
+            cached.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return None
+
+        return self.app.cache.get_guild(self.guild_id)
diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py
index 92b6917c..99fa7c6d 100644
--- a/hikari/impl/event_factory.py
+++ b/hikari/impl/event_factory.py
@@ -480,7 +480,7 @@ def deserialize_message_create_event(
 
         return message_events.GuildMessageCreateEvent(shard=shard, message=message)
 
-    def deserialize_message_update_event(  # noqa: CFQ001
+    def deserialize_message_update_event(
         self,
         shard: gateway_shard.GatewayShard,
         payload: data_binding.JSONObject,
@@ -495,48 +495,46 @@ def deserialize_message_update_event(  # noqa: CFQ001
         return message_events.GuildMessageUpdateEvent(shard=shard, message=message, old_message=old_message)
 
     def deserialize_message_delete_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_message: typing.Optional[messages_models.Message],
     ) -> message_events.MessageDeleteEvent:
         channel_id = snowflakes.Snowflake(payload["channel_id"])
-        message_ids = collections.SnowflakeSet(int(payload["id"]))
+        message_id = snowflakes.Snowflake(payload["id"])
 
         if "guild_id" in payload:
             return message_events.GuildMessageDeleteEvent(
                 app=self._app,
                 shard=shard,
                 channel_id=channel_id,
-                message_ids=message_ids,
-                is_bulk=False,
+                message_id=message_id,
                 guild_id=snowflakes.Snowflake(payload["guild_id"]),
+                old_message=old_message,
             )
 
         return message_events.DMMessageDeleteEvent(
             app=self._app,
             shard=shard,
             channel_id=channel_id,
-            message_ids=message_ids,
-            is_bulk=False,
+            message_id=message_id,
+            old_message=old_message,
         )
 
-    def deserialize_message_delete_bulk_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
-    ) -> message_events.MessageDeleteEvent:
-
-        message_ids = collections.SnowflakeSet(*(snowflakes.Snowflake(message_id) for message_id in payload["ids"]))
-        channel_id = snowflakes.Snowflake(payload["channel_id"])
-
-        if "guild_id" in payload:
-            return message_events.GuildMessageDeleteEvent(
-                app=self._app,
-                shard=shard,
-                channel_id=channel_id,
-                guild_id=snowflakes.Snowflake(payload["guild_id"]),
-                message_ids=message_ids,
-                is_bulk=True,
-            )
-
-        return message_events.DMMessageDeleteEvent(
-            app=self._app, shard=shard, channel_id=channel_id, message_ids=message_ids, is_bulk=True
+    def deserialize_guild_message_delete_bulk_event(
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        old_messages: typing.Mapping[snowflakes.Snowflake, messages_models.Message],
+    ) -> message_events.GuildBulkMessageDeleteEvent:
+        return message_events.GuildBulkMessageDeleteEvent(
+            app=self._app,
+            shard=shard,
+            channel_id=snowflakes.Snowflake(payload["channel_id"]),
+            guild_id=snowflakes.Snowflake(payload["guild_id"]),
+            message_ids=collections.SnowflakeSet(*(snowflakes.Snowflake(message_id) for message_id in payload["ids"])),
+            old_messages=old_messages,
         )
 
     ###################
diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py
index bcd72d9e..25c43255 100644
--- a/hikari/impl/event_manager.py
+++ b/hikari/impl/event_manager.py
@@ -389,22 +389,30 @@ async def on_message_update(self, shard: gateway_shard.GatewayShard, payload: da
 
     async def on_message_delete(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#message-delete for more info."""
-        event = self._event_factory.deserialize_message_delete_event(shard, payload)
-
         if self._cache:
-            self._cache.delete_message(event.message_id)
+            message_id = snowflakes.Snowflake(payload["id"])
+            old_message = self._cache.delete_message(message_id)
+        else:
+            old_message = None
+
+        event = self._event_factory.deserialize_message_delete_event(shard, payload, old_message=old_message)
 
         await self.dispatch(event)
 
     async def on_message_delete_bulk(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#message-delete-bulk for more info."""
-        event = self._event_factory.deserialize_message_delete_bulk_event(shard, payload)
+        old_messages = {}
 
         if self._cache:
-            for message_id in event.message_ids:
-                self._cache.delete_message(message_id)
+            for message_id in payload["ids"]:
+                message_id = snowflakes.Snowflake(message_id)
 
-        await self.dispatch(event)
+                if message := self._cache.delete_message(message_id):
+                    old_messages[message_id] = message
+
+        await self.dispatch(
+            self._event_factory.deserialize_guild_message_delete_bulk_event(shard, payload, old_messages=old_messages)
+        )
 
     async def on_message_reaction_add(
         self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
diff --git a/requirements.txt b/requirements.txt
index 4321bf8b..1fa5f195 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-aiohttp==3.8.0
+aiohttp==3.8.1
 attrs==21.2.0
 colorlog==6.6.0
 multidict==5.2.0


Test patch:
diff --git a/tests/hikari/events/test_message_events.py b/tests/hikari/events/test_message_events.py
index e59bea4a..5476efa2 100644
--- a/tests/hikari/events/test_message_events.py
+++ b/tests/hikari/events/test_message_events.py
@@ -319,17 +319,6 @@ def test_old_message(self, event):
         assert event.old_message.id == 123
 
 
-class TestMessageDeleteEvent:
-    def test_message_id_property(self):
-        event = hikari_test_helpers.mock_class_namespace(message_events.MessageDeleteEvent, message_ids=[9, 18, 27])()
-        assert event.message_id == 9
-
-    def test_message_id_property_if_empty_errors(self):
-        event = hikari_test_helpers.mock_class_namespace(message_events.MessageDeleteEvent, message_ids=[])()
-        with pytest.raises(RuntimeError):
-            _ = event.message_id
-
-
 class TestGuildMessageDeleteEvent:
     @pytest.fixture()
     def event(self):
@@ -337,9 +326,9 @@ def event(self):
             guild_id=snowflakes.Snowflake(542342354564),
             channel_id=snowflakes.Snowflake(54213123123),
             app=mock.Mock(),
-            shard=mock.Mock(),
-            message_ids={9, 18, 27, 36},
-            is_bulk=True,
+            shard=object(),
+            message_id=9,
+            old_message=object(),
         )
 
     def test_get_channel_when_no_cache_trait(self, event):
diff --git a/tests/hikari/impl/test_event_factory.py b/tests/hikari/impl/test_event_factory.py
index a601fa0d..eece3e08 100644
--- a/tests/hikari/impl/test_event_factory.py
+++ b/tests/hikari/impl/test_event_factory.py
@@ -615,54 +615,47 @@ def test_deserialize_message_update_event_in_dm(self, event_factory, mock_app, m
 
     def test_deserialize_message_delete_event_in_guild(self, event_factory, mock_app, mock_shard):
         mock_payload = {"id": "5412", "channel_id": "541123", "guild_id": "9494949"}
+        old_message = object()
 
-        event = event_factory.deserialize_message_delete_event(mock_shard, mock_payload)
+        event = event_factory.deserialize_message_delete_event(mock_shard, mock_payload, old_message=old_message)
 
         assert isinstance(event, message_events.GuildMessageDeleteEvent)
         assert event.app is mock_app
         assert event.shard is mock_shard
+        assert event.old_message is old_message
         assert event.channel_id == 541123
-        assert event.message_ids == {5412}
-        assert event.is_bulk is False
+        assert event.message_id == 5412
         assert event.guild_id == 9494949
 
     def test_deserialize_message_delete_event_in_dm(self, event_factory, mock_app, mock_shard):
         mock_payload = {"id": "5412", "channel_id": "541123"}
+        old_message = object()
 
-        event = event_factory.deserialize_message_delete_event(mock_shard, mock_payload)
+        event = event_factory.deserialize_message_delete_event(mock_shard, mock_payload, old_message=old_message)
 
         assert isinstance(event, message_events.DMMessageDeleteEvent)
         assert event.app is mock_app
         assert event.shard is mock_shard
+        assert event.old_message is old_message
         assert event.channel_id == 541123
-        assert event.message_ids == {5412}
-        assert event.is_bulk is False
+        assert event.message_id == 5412
 
-    def test_deserialize_message_delete_bulk_event_in_guild(self, event_factory, mock_app, mock_shard):
+    def test_deserialize_guild_message_delete_bulk_event(self, event_factory, mock_app, mock_shard):
         mock_payload = {"ids": ["6523423", "345123"], "channel_id": "564123", "guild_id": "4394949"}
+        old_messages = object()
 
-        event = event_factory.deserialize_message_delete_bulk_event(mock_shard, mock_payload)
+        event = event_factory.deserialize_guild_message_delete_bulk_event(
+            mock_shard, mock_payload, old_messages=old_messages
+        )
 
-        assert isinstance(event, message_events.GuildMessageDeleteEvent)
+        assert isinstance(event, message_events.GuildBulkMessageDeleteEvent)
         assert event.app is mock_app
         assert event.shard is mock_shard
+        assert event.old_messages is old_messages
         assert event.channel_id == 564123
         assert event.message_ids == {6523423, 345123}
-        assert event.is_bulk is True
         assert event.guild_id == 4394949
 
-    def test_deserialize_message_delete_bulk_event_in_dm(self, event_factory, mock_app, mock_shard):
-        mock_payload = {"ids": ["6523423", "345123"], "channel_id": "564123"}
-
-        event = event_factory.deserialize_message_delete_bulk_event(mock_shard, mock_payload)
-
-        assert isinstance(event, message_events.DMMessageDeleteEvent)
-        assert event.app is mock_app
-        assert event.shard is mock_shard
-        assert event.channel_id == 564123
-        assert event.message_ids == {6523423, 345123}
-        assert event.is_bulk is True
-
     ###################
     # REACTION EVENTS #
     ###################
diff --git a/tests/hikari/impl/test_event_manager.py b/tests/hikari/impl/test_event_manager.py
index 9840fc76..c6b8ea51 100644
--- a/tests/hikari/impl/test_event_manager.py
+++ b/tests/hikari/impl/test_event_manager.py
@@ -926,41 +926,45 @@ async def test_on_message_update_stateless(self, stateless_event_manager, shard,
         )
 
     @pytest.mark.asyncio()
-    async def test_on_message_delete_stateless(self, event_manager, shard, event_factory):
-        payload = {}
-        event = mock.Mock(message_id=123)
-
-        event_factory.deserialize_message_delete_event.return_value = event
+    async def test_on_message_delete_stateful(self, event_manager, shard, event_factory):
+        payload = {"id": 123}
 
         await event_manager.on_message_delete(shard, payload)
 
         event_manager._cache.delete_message.assert_called_once_with(123)
-        event_factory.deserialize_message_delete_event.assert_called_once_with(shard, payload)
-        event_manager.dispatch.assert_awaited_once_with(event)
+        event_factory.deserialize_message_delete_event.assert_called_once_with(
+            shard, payload, old_message=event_manager._cache.delete_message.return_value
+        )
+        event_manager.dispatch.assert_awaited_once_with(event_factory.deserialize_message_delete_event.return_value)
 
     @pytest.mark.asyncio()
-    async def test_on_message_delete_stateful(self, stateless_event_manager, shard, event_factory):
+    async def test_on_message_delete_stateless(self, stateless_event_manager, shard, event_factory):
         payload = {}
 
         await stateless_event_manager.on_message_delete(shard, payload)
 
-        event_factory.deserialize_message_delete_event.assert_called_once_with(shard, payload)
+        event_factory.deserialize_message_delete_event.assert_called_once_with(shard, payload, old_message=None)
         stateless_event_manager.dispatch.assert_awaited_once_with(
             event_factory.deserialize_message_delete_event.return_value
         )
 
     @pytest.mark.asyncio()
     async def test_on_message_delete_bulk_stateful(self, event_manager, shard, event_factory):
-        payload = {}
-        event = mock.Mock(message_ids=[123, 456, 789])
-
-        event_factory.deserialize_message_delete_bulk_event.return_value = event
+        payload = {"ids": [123, 456, 789]}
+        message1 = object()
+        message2 = object()
+        message3 = object()
+        event_manager._cache.delete_message.side_effect = [message1, message2, message3]
 
         await event_manager.on_message_delete_bulk(shard, payload)
 
         event_manager._cache.delete_message.assert_has_calls([mock.call(123), mock.call(456), mock.call(789)])
-        event_factory.deserialize_message_delete_bulk_event.assert_called_once_with(shard, payload)
-        event_manager.dispatch.assert_awaited_once_with(event)
+        event_factory.deserialize_guild_message_delete_bulk_event.assert_called_once_with(
+            shard, payload, old_messages={123: message1, 456: message2, 789: message3}
+        )
+        event_manager.dispatch.assert_awaited_once_with(
+            event_factory.deserialize_guild_message_delete_bulk_event.return_value
+        )
 
     @pytest.mark.asyncio()
     async def test_on_message_delete_bulk_stateless(self, stateless_event_manager, shard, event_factory):
@@ -968,9 +972,11 @@ async def test_on_message_delete_bulk_stateless(self, stateless_event_manager, s
 
         await stateless_event_manager.on_message_delete_bulk(shard, payload)
 
-        event_factory.deserialize_message_delete_bulk_event.assert_called_once_with(shard, payload)
+        event_factory.deserialize_guild_message_delete_bulk_event.assert_called_once_with(
+            shard, payload, old_messages={}
+        )
         stateless_event_manager.dispatch.assert_awaited_once_with(
-            event_factory.deserialize_message_delete_bulk_event.return_value
+            event_factory.deserialize_guild_message_delete_bulk_event.return_value
         )
 
     @pytest.mark.asyncio()