Repository: hikari-py/hikari
Pull request #369 (2020-11-22 10:01:51)

Switch to using `x-ratelimit-reset-after` as reset_after
### Summary
Switch to using `x-ratelimit-reset-after` as reset_after
* This is generally speaking more accurate than calculating this value from the `Date` header
  and `X-RateLimit-Reset` headers as these seem to drift away and on add reaction endpoints
  were seen to by almost a second larger than `x-ratelimit-reset-after` in some cases
  (e.g 0.25 vs 0.403, 0.25 vs 0.425, 0.25 vs 1.098, 0.25 vs 0.361 and 0.25 vs 0.634)
* Make owner mandatory on application

### 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)
-->



Patch:
diff --git a/docs/documentation.mako b/docs/documentation.mako
index 0080a20a..87861f28 100644
--- a/docs/documentation.mako
+++ b/docs/documentation.mako
@@ -227,74 +227,56 @@
         if code_span.startswith("`") and code_span.endswith("`"):
             code_span = codespan[1:-1]
 
-        try:
-            # If it doesn't parse, it isn't a valid reference.
-            ast.parse(code_span)
-
-            # Extract identifiers.
-            items = list(re.finditer(r"(\w|\.)+", code_span))[::-1]
+        # Extract identifiers.
+        items = list(re.finditer(r"(\w|\.)+", code_span))[::-1]
 
-            # For each identifier, replace it with a link.
-            for match in items:
-                phrase = match.group()
+        # For each identifier, replace it with a link.
+        for match in items:
+            phrase = match.group()
 
-                ident = module.find_ident(phrase)
+            ident = module.find_ident(phrase)
 
-                if isinstance(ident, pdoc.External):
-                    phrase = debuiltinify(ident.name)
+            if isinstance(ident, pdoc.External):
+                phrase = debuiltinify(ident.name)
 
-                    url = get_url_for_object_from_imports(phrase, ident)
+                url = get_url_for_object_from_imports(phrase, ident)
 
-                    if url is None:
-                        module_part = module.find_ident(phrase.split('.')[0])
-                        if not isinstance(module_part, pdoc.External):
-                            print(f"Code reference `{phrase}` in module '{module.refname}' does not match any documented object.")
-                            print("Type", module_part.__class__, module_part)
+                if url is None:
+                    module_part = module.find_ident(phrase.split('.')[0])
+                    if not isinstance(module_part, pdoc.External):
+                        print(f"Code reference `{phrase}` in module '{module.refname}' does not match any documented object.")
+                        print("Type", module_part.__class__, module_part)
 
-                        bits = ident.name.split(".")[:-1]
+                    bits = ident.name.split(".")[:-1]
 
-                        while bits:
-                            partial_phrase = ".".join(bits)
-                            if partial_phrase in pdoc._global_context:
-                                url = pdoc._global_context[partial_phrase].url(relative_to=module, link_prefix=link_prefix, top_ancestor=not show_inherited_members)
-                                a_tag = f"<a href={url!r}>{repr(phrase)[1:-1]}</a>"
-                                break
+                    while bits:
+                        partial_phrase = ".".join(bits)
+                        if partial_phrase in pdoc._global_context:
+                            url = pdoc._global_context[partial_phrase].url(relative_to=module, link_prefix=link_prefix, top_ancestor=not show_inherited_members)
+                            a_tag = f"<a href={url!r}>{repr(phrase)[1:-1]}</a>"
+                            break
 
-                            bits = bits[:-1]
-                        else:
-                            a_tag = phrase
+                        bits = bits[:-1]
                     else:
-                        a_tag = f"<a href={url!r}>{repr(phrase)[1:-1]}</a>"
-
-
+                        a_tag = phrase
                 else:
-                    url = ident.url(relative_to=module, link_prefix=link_prefix, top_ancestor=not show_inherited_members)
-                    a_tag = f"<a href={url!r}>{repr(ident.name)[1:-1]}</a>"
-
-                chunk = slice(*match.span())
-                code_span = "".join((
-                    code_span[:chunk.start],
-                    a_tag,
-                    code_span[chunk.stop:],
-                ))
+                    a_tag = f"<a href={url!r}>{repr(phrase)[1:-1]}</a>"
 
-            if wrap_code:
-                code_span = code_span.replace('[', '\\[')
-
-
-        except SyntaxError:
-            """Raised if it wasn't a valid piece of Python that was given.
-
-            This can happen with `builtins.True`, `builtins.False` or
-            `builtins.None`
-            """
-            if code_span.startswith("builtins."):
-                phrase = debuiltinify(code_span)
-                url = discover_source(code_span)
-                code_span = f"<a href={url!r}>{repr(phrase)[1:-1]}</a>"
 
+            else:
+                url = ident.url(relative_to=module, link_prefix=link_prefix, top_ancestor=not show_inherited_members)
+                a_tag = f"<a href={url!r}>{repr(ident.name)[1:-1]}</a>"
+
+            chunk = slice(*match.span())
+            code_span = "".join((
+                code_span[:chunk.start],
+                a_tag,
+                code_span[chunk.stop:],
+            ))
 
         if wrap_code:
+            code_span = code_span.replace('[', '\\[')
+
             # Wrapping in HTML <code> as opposed to backticks evaluates markdown */_ markers,
             # so let's escape them in text (but not in HTML tag attributes).
             # Backticks also cannot be used because html returned from `link()`
diff --git a/hikari/api/cache.py b/hikari/api/cache.py
index ec2bf436..3b947e02 100644
--- a/hikari/api/cache.py
+++ b/hikari/api/cache.py
@@ -34,6 +34,7 @@
     from hikari import emojis
     from hikari import guilds
     from hikari import invites
+    from hikari import messages
     from hikari import presences
     from hikari import snowflakes
     from hikari import users
@@ -1584,3 +1585,103 @@ def update_voice_state(
         builtins.NotImplementedError
             When called on a stateless cache implementation.
         """
+
+    @abc.abstractmethod
+    def clear_messages(self) -> CacheView[snowflakes.Snowflake, messages.Message]:
+        """Remove all message objects from the cache.
+
+        Returns
+        -------
+        CacheView[hikari.snowflakes.Snowflake, hikari.messages.Message]
+            A view of message objects that were removed from the cache.
+
+        Raises
+        ------
+        builtins.NotImplementedError
+            When called on a stateless cache implementation.
+        """
+
+    @abc.abstractmethod
+    def delete_message(self, message_id: snowflakes.Snowflake, /) -> typing.Optional[messages.Message]:
+        """Remove a message object from the cache.
+
+        Parameters
+        ----------
+        message_id : hikari.snowflakes.Snowflake
+            The ID of the messages to remove the cache.
+
+        Returns
+        -------
+        typing.Optional[hikari.messages.Message]
+            The object of the message that was removed from the cache if found,
+            else `builtins.None`.
+
+        Raises
+        ------
+        builtins.NotImplementedError
+            When called on a stateless cache implementation.
+        """
+
+    @abc.abstractmethod
+    def get_message(self, message_id: snowflakes.Snowflake, /) -> typing.Optional[messages.Message]:
+        """Get a message object from the cache.
+
+        Parameters
+        ----------
+        message_id : hikari.snowflakes.Snowflake
+            The ID of the message to get from the cache.
+
+        Returns
+        -------
+        typing.Optional[hikari.messages.Message]
+            The object of the message found in the cache or `builtins.None`.
+        """
+
+    @abc.abstractmethod
+    def get_messages_view(self) -> CacheView[snowflakes.Snowflake, messages.Message]:
+        """Get a view of all the message objects in the cache.
+
+        Returns
+        -------
+        CacheView[hikari.snowflakes.Snowflake, hikari.messages.Message]
+            A view of message objects found in the cache.
+        """
+
+    @abc.abstractmethod
+    def set_message(self, message: messages.Message, /) -> None:
+        """Add a message object to the cache.
+
+        Parameters
+        ----------
+        message : hikari.messages.Message
+            The object of the message to add to the cache.
+
+        Raises
+        ------
+        builtins.NotImplementedError
+            When called on a stateless cache implementation.
+        """
+
+    @abc.abstractmethod
+    def update_message(
+        self, message: typing.Union[messages.PartialMessage, messages.Message], /
+    ) -> typing.Tuple[typing.Optional[messages.Message], typing.Optional[messages.Message]]:
+        """Update a message in the cache.
+
+        Parameters
+        ----------
+        message : typing.Union[hikari.messages.PartialMessage, hikari.messages.Message]
+            The object of the message to update in the cache.
+
+        Returns
+        -------
+        typing.Tuple[typing.Optional[hikari.messages.Message], typing.Optional[hikari.messages.Message]]
+            A tuple of the old cached message object if found (else `builtins.None`)
+            and the new cached message object if it could be cached (else
+            `builtins.None`).
+
+        Raises
+        ------
+        builtins.NotImplementedError
+            When called on a stateless cache implementation.
+        """
diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py
index 226df359..9ec208c6 100644
--- a/hikari/api/event_dispatcher.py
+++ b/hikari/api/event_dispatcher.py
@@ -361,8 +361,8 @@ async def wait_for(
             If left as `None` (the default), then the first matching event type
             that the bot receives (or any subtype) will be the one returned.
 
-            !!! warn
-                ASYNC PREDICATES ARE NOT SUPPORTED.
+            !!! warning
+                Async predicates are not supported.
         timeout : typing.Union[builtins.float, builtins.int, builtins.None]
             The amount of time to wait before raising an `asyncio.TimeoutError`
             and giving up instead. This is measured in seconds. If
@@ -387,4 +387,4 @@ async def wait_for(
         Stream: `hikari.api.event_dispatcher.EventDispatcher.stream`
         Subscribe: `hikari.api.event_dispatcher.EventDispatcher.subscribe`
         Dispatch: `hikari.api.event_dispatcher.EventDispatcher.dispatch`
-        """
+        """  # noqa: E501 - Line too long
diff --git a/hikari/api/event_factory.py b/hikari/api/event_factory.py
index 2362ca76..929e0e31 100644
--- a/hikari/api/event_factory.py
+++ b/hikari/api/event_factory.py
@@ -28,6 +28,13 @@
 import typing
 
 if typing.TYPE_CHECKING:
+    from hikari import channels as channel_models
+    from hikari import emojis as emojis_models
+    from hikari import guilds as guild_models
+    from hikari import messages as messages_models
+    from hikari import presences as presences_models
+    from hikari import users as user_models
+    from hikari import voices as voices_models
     from hikari.api import shard as gateway_shard
     from hikari.events import channel_events
     from hikari.events import guild_events
@@ -70,7 +77,11 @@ def deserialize_channel_create_event(
         """
 
     def deserialize_channel_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_channel: typing.Optional[channel_models.GuildChannel],
     ) -> channel_events.ChannelUpdateEvent:
         """Parse a raw payload from Discord into a channel update event object.
 
@@ -80,6 +91,8 @@ def deserialize_channel_update_event(
             The shard that emitted this event.
         payload : hikari.internal.data_binding.JSONObject
             The dict payload to parse.
+        old_channel : typing.Optional[hikari.channels.GuildChannel]
+            The guild channel object or `builtins.None`.
 
         Returns
         -------
@@ -218,7 +231,11 @@ def deserialize_guild_create_event(
         """
 
     def deserialize_guild_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_guild: typing.Optional[guild_models.Guild],
     ) -> guild_events.GuildUpdateEvent:
         """Parse a raw payload from Discord into a guild update event object.
 
@@ -228,6 +245,8 @@ def deserialize_guild_update_event(
             The shard that emitted this event.
         payload : hikari.internal.data_binding.JSONObject
             The dict payload to parse.
+        old_guild : typing.Optional[hikari.guilds.Guild]
+            The guild object or `builtins.None`.
 
         Returns
         -------
@@ -308,7 +327,11 @@ def deserialize_guild_ban_remove_event(
         """
 
     def deserialize_guild_emojis_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_emojis: typing.Optional[typing.Sequence[emojis_models.KnownCustomEmoji]],
     ) -> guild_events.EmojisUpdateEvent:
         """Parse a raw payload from Discord into a guild emojis update event object.
 
@@ -318,6 +341,8 @@ def deserialize_guild_emojis_update_event(
             The shard that emitted this event.
         payload : hikari.internal.data_binding.JSONObject
             The dict payload to parse.
+        old_emojis : typing.Optional[typing.Sequence[hikari.emojis.KnownCustomEmoji]]
+            The sequence of emojis or `builtins.None`.
 
         Returns
         -------
@@ -398,7 +423,11 @@ def deserialize_guild_member_add_event(
         """
 
     def deserialize_guild_member_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_member: typing.Optional[guild_models.Member],
     ) -> member_events.MemberUpdateEvent:
         """Parse a raw payload from Discord into a guild member update event object.
 
@@ -408,6 +437,8 @@ def deserialize_guild_member_update_event(
             The shard that emitted this event.
         payload : hikari.internal.data_binding.JSONObject
             The dict payload to parse.
+        old_member: typing.Optional[hikari.guilds.Member]
+            The member object or `builtins.None`.
 
         Returns
         -------
@@ -452,7 +483,11 @@ def deserialize_guild_role_create_event(
         """
 
     def deserialize_guild_role_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_role: typing.Optional[guild_models.Role],
     ) -> role_events.RoleUpdateEvent:
         """Parse a raw payload from Discord into a guild role update event object.
 
@@ -462,6 +497,8 @@ def deserialize_guild_role_update_event(
             The shard that emitted this event.
         payload : hikari.internal.data_binding.JSONObject
             The dict payload to parse.
+        old_role: typing.Optional[hikari.guilds.Role]
+            The role object or `builtins.None`.
 
         Returns
         -------
@@ -488,7 +525,11 @@ def deserialize_guild_role_delete_event(
         """
 
     def deserialize_presence_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_presence: typing.Optional[presences_models.MemberPresence],
     ) -> guild_events.PresenceUpdateEvent:
         """Parse a raw payload from Discord into a presence update event object.
 
@@ -498,6 +539,8 @@ def deserialize_presence_update_event(
             The shard that emitted this event.
         payload : hikari.internal.data_binding.JSONObject
             The dict payload to parse.
+        old_presence: typing.Optional[hikari.presences.MemberPresence]
+            The presence object or `builtins.None`.
 
         Returns
         -------
@@ -528,7 +571,11 @@ def deserialize_message_create_event(
         """
 
     def deserialize_message_update_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.PartialMessage],
     ) -> message_events.MessageUpdateEvent:
         """Parse a raw payload from Discord into a message update event object.
 
@@ -538,6 +585,8 @@ def deserialize_message_update_event(
             The shard that emitted this event.
         payload : hikari.internal.data_binding.JSONObject
             The dict payload to parse.
+        old_message: typing.Optional[hikari.messages.PartialMessage]
+            The message object or `builtins.None`.
 
         Returns
         -------
@@ -683,7 +732,11 @@ def deserialize_ready_event(
         """
 
     def deserialize_own_user_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_user: typing.Optional[user_models.OwnUser],
     ) -> user_events.OwnUserUpdateEvent:
         """Parse a raw payload from Discord into a own user update event object.
 
@@ -693,6 +746,8 @@ def deserialize_own_user_update_event(
             The shard that emitted this event.
         payload : hikari.internal.data_binding.JSONObject
             The dict payload to parse.
+        old_user: typing.Optional[hikari.users.OwnUser]
+            The OwnUser object or `builtins.None`.
 
         Returns
         -------
@@ -723,7 +778,11 @@ def deserialize_guild_member_chunk_event(
     ################
 
     def deserialize_voice_state_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_state: typing.Optional[voices_models.VoiceState],
     ) -> voice_events.VoiceStateUpdateEvent:
         """Parse a raw payload from Discord into a voice state update event object.
 
@@ -733,6 +792,8 @@ def deserialize_voice_state_update_event(
             The shard that emitted this event.
         payload : hikari.internal.data_binding.JSONObject
             The dict payload to parse.
+        old_state: typing.Optional[hikari.voices.VoiceState]
+            The VoiceState object or `builtins.None`.
 
         Returns
         -------
diff --git a/hikari/api/rest.py b/hikari/api/rest.py
index 321732ff..a992e892 100644
--- a/hikari/api/rest.py
+++ b/hikari/api/rest.py
@@ -3224,7 +3224,7 @@ async def edit_guild(
         owner : hikari.undefined.UndefinedOr[hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]]]
             If provided, the new guild owner.
 
-            !!! warn
+            !!! warning
                 You need to be the owner of the server to use this.
         splash : hikari.undefined.UndefinedNoneOr[hikari.files.Resourceish]
             If provided, the new guild splash. Must be a 16:9 image and the
diff --git a/hikari/applications.py b/hikari/applications.py
index f21c529e..6e327551 100644
--- a/hikari/applications.py
+++ b/hikari/applications.py
@@ -120,7 +120,7 @@ class OAuth2Scope(str, enums.Enum):
     GROUP_DM_JOIN = "gdm.join"
     """Enables joining users into a group DM.
 
-    !!! warn
+    !!! warning
         This cannot add the bot to a group DM.
     """
 
@@ -383,12 +383,8 @@ class Application(guilds.PartialApplication):
     Will be `builtins.None` if this application doesn't have a bot.
     """
 
-    owner: typing.Optional[users.User] = attr.ib(eq=False, hash=False, repr=True)
-    """The application's owner.
-
-    This should always be `builtins.None` in application objects retrieved outside
-    Discord's oauth2 flow.
-    """
+    owner: users.User = attr.ib(eq=False, hash=False, repr=True)
+    """The application's owner."""
 
     rpc_origins: typing.Optional[typing.Sequence[str]] = attr.ib(eq=False, hash=False, repr=False)
     """A collection of this application's RPC origin URLs, if RPC is enabled."""
diff --git a/hikari/config.py b/hikari/config.py
index 3df75b31..9d26d081 100644
--- a/hikari/config.py
+++ b/hikari/config.py
@@ -28,6 +28,7 @@
     "ProxySettings",
     "HTTPTimeoutSettings",
     "HTTPSettings",
+    "CacheSettings",
 ]
 
 import base64
@@ -388,3 +389,15 @@ class that will **NOT** enforce SSL verification. This is then stored
     HTTPTimeoutSettings
         The HTTP timeout settings to use for connection timeouts.
     """
+
+
+@attr_extensions.with_copy
+@attr.s(slots=True, kw_only=True, weakref_slot=False)
+class CacheSettings:
+    """Settings to control the cache."""
+
+    max_messages: int = attr.ib(default=300)
+    """The max number of messages to store in the cache at once.
+
+    Defaults to `300`.
+    """
diff --git a/hikari/embeds.py b/hikari/embeds.py
index 88a4b70f..f344170d 100644
--- a/hikari/embeds.py
+++ b/hikari/embeds.py
@@ -526,11 +526,7 @@ def __warn_naive_datetime() -> None:
             The offending line of code causing this warning was:"""
         )
 
-        warnings.warn(
-            message,
-            category=errors.HikariWarning,
-            stacklevel=3,
-        )
+        warnings.warn(message, category=errors.HikariWarning, stacklevel=3)
 
     @property
     def footer(self) -> typing.Optional[EmbedFooter]:
diff --git a/hikari/events/channel_events.py b/hikari/events/channel_events.py
index bd7e6c89..4600d924 100644
--- a/hikari/events/channel_events.py
+++ b/hikari/events/channel_events.py
@@ -279,6 +279,12 @@ class GuildChannelUpdateEvent(GuildChannelEvent, ChannelUpdateEvent):
     shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
     # <<inherited docstring from ShardEvent>>.
 
+    old_channel: typing.Optional[channels.GuildChannel] = attr.ib(repr=True)
+    """The old guild channel object.
+
+    This will be `builtins.None` if the channel missing from the cache.
+    """
+
     channel: channels.GuildChannel = attr.ib(repr=True)
     """Guild channel that this event represents.
 
diff --git a/hikari/events/guild_events.py b/hikari/events/guild_events.py
index 6bba5864..ff4edbc6 100644
--- a/hikari/events/guild_events.py
+++ b/hikari/events/guild_events.py
@@ -268,6 +268,12 @@ class GuildUpdateEvent(GuildEvent):
     shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
     # <<inherited docstring from ShardEvent>>.
 
+    old_guild: typing.Optional[guilds.Guild] = attr.ib()
+    """The old guild object.
+
+    This will be `builtins.None` if the guild missing from the cache.
+    """
+
     guild: guilds.GatewayGuild = attr.ib()
     """Guild that was just updated.
 
@@ -393,6 +399,12 @@ class EmojisUpdateEvent(GuildEvent):
     guild_id: snowflakes.Snowflake = attr.ib()
     # <<inherited docstring from GuildEvent>>.
 
+    old_emojis: typing.Optional[typing.Sequence[emojis_.KnownCustomEmoji]] = attr.ib()
+    """Sequence of all old emojis in this guild.
+
+    This will be `builtins.None` if it's missing from the cache.
+    """
+
     emojis: typing.Sequence[emojis_.KnownCustomEmoji] = attr.ib()
     """Sequence of all emojis in this guild.
 
@@ -566,6 +578,12 @@ class PresenceUpdateEvent(shard_events.ShardEvent):
     shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
     # <<inherited docstring from ShardEvent>>.
 
+    old_presence: typing.Optional[presences_.MemberPresence] = attr.ib()
+    """The old member presence object.
+
+    This will be `builtins.None` if the member presence missing from the cache.
+    """
+
     presence: presences_.MemberPresence = attr.ib()
     """Member presence.
 
diff --git a/hikari/events/lifetime_events.py b/hikari/events/lifetime_events.py
index 42c6d9c3..289ec964 100644
--- a/hikari/events/lifetime_events.py
+++ b/hikari/events/lifetime_events.py
@@ -45,7 +45,7 @@
 class StartingEvent(base_events.Event):
     """Event that is triggered before the application connects to discord.
 
-    This will only fire once per `_rest.run`/`_rest.start`, so is suitable for
+    This will only fire once per `bot.run` / `bot.start`, so is suitable for
     opening database connections and other resources that need to be
     initialized within a coroutine function.
 
@@ -57,12 +57,6 @@ class StartingEvent(base_events.Event):
 
     If you want to do something _after_ the application has initialized, you
     should consider using `StartedEvent` instead.
-
-    See Also
-    --------
-    `StartedEvent`
-    `StoppingEvent`
-    `StoppedEvent`
     """
 
     app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
@@ -74,18 +68,12 @@ class StartingEvent(base_events.Event):
 class StartedEvent(base_events.Event):
     """Event that is triggered after the application has started.
 
-    This will only fire once per `_rest.run`/`_rest.start`, so is suitable for
+    This will only fire once per `bot.run` / `bot.start`, so is suitable for
     opening database connections and other resources that need to be
     initialized within a coroutine function.
 
     If you want to do something _before_ the application connects, you should
     consider using `StartingEvent` instead.
-
-    See Also
-    --------
-    `StartingEvent`
-    `StoppingEvent`
-    `StoppedEvent`
     """
 
     app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
@@ -99,7 +87,7 @@ class StoppingEvent(base_events.Event):
 
     This will fire before the connection is physically disconnected.
 
-    This will only fire once per `_rest.close`, so is suitable for
+    This will only fire once per `bot.close`, so is suitable for
     closing database connections and other resources that need to be
     closed within a coroutine function.
 
@@ -111,12 +99,6 @@ class StoppingEvent(base_events.Event):
 
     If you want to do something _after_ the disconnection has occurred, you
     should consider using `StoppedEvent` instead.
-
-    See Also
-    --------
-    `StartingEvent`
-    `StartedEvent`
-    `StoppedEvent`
     """
 
     app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
@@ -128,7 +110,7 @@ class StoppingEvent(base_events.Event):
 class StoppedEvent(base_events.Event):
     """Event that is triggered once the application has disconnected.
 
-    This will only fire once per `_rest.close`, so is suitable for
+    This will only fire once per `bot.close`, so is suitable for
     closing database connections and other resources that need to be
     closed within a coroutine function.
 
@@ -141,12 +123,6 @@ class StoppedEvent(base_events.Event):
     If you want to do something when the application is preparing to shut down,
     but _before_ any connection to discord is closed, you should consider using
     `StoppingEvent` instead.
-
-    See Also
-    --------
-    `StartingEvent`
-    `StartedEvent`
-    `StoppingEvent`
     """
 
     app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
diff --git a/hikari/events/member_events.py b/hikari/events/member_events.py
index 1c15a20c..23395ab0 100644
--- a/hikari/events/member_events.py
+++ b/hikari/events/member_events.py
@@ -147,6 +147,12 @@ class MemberUpdateEvent(MemberEvent):
     shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
     # <<inherited docstring from ShardEvent>>.
 
+    old_member: typing.Optional[guilds.Member] = attr.ib()
+    """The old member object.
+
+    This will be `builtins.None` if the member missing from the cache.
+    """
+
     member: guilds.Member = attr.ib()
     """Member object for the member that was updated.
 
diff --git a/hikari/events/message_events.py b/hikari/events/message_events.py
index 30e15ce6..ae95bb81 100644
--- a/hikari/events/message_events.py
+++ b/hikari/events/message_events.py
@@ -455,6 +455,12 @@ class GuildMessageUpdateEvent(MessageUpdateEvent):
     app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
     # <<inherited docstring from Event>>
 
+    old_message: typing.Optional[messages.PartialMessage] = attr.ib()
+    """The old message object.
+
+    This will be `builtins.None` if the message missing from the cache.
+    """
+
     message: messages.PartialMessage = attr.ib()
     # <<inherited docstring from MessageUpdateEvent>>
 
@@ -551,6 +557,12 @@ class DMMessageUpdateEvent(MessageUpdateEvent):
     app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
     # <<inherited docstring from Event>>
 
+    old_message: typing.Optional[messages.PartialMessage] = attr.ib()
+    """The old message object.
+
+    This will be `builtins.None` if the message missing from the cache.
+    """
+
     message: messages.PartialMessage = attr.ib()
     # <<inherited docstring from MessageUpdateEvent>>
 
diff --git a/hikari/events/role_events.py b/hikari/events/role_events.py
index 4ec8852e..b2a4a33b 100644
--- a/hikari/events/role_events.py
+++ b/hikari/events/role_events.py
@@ -118,6 +118,12 @@ class RoleUpdateEvent(RoleEvent):
     shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
     # <<inherited docstring from ShardEvent>>.
 
+    old_role: typing.Optional[guilds.Role] = attr.ib()
+    """The old role object.
+
+    This will be `builtins.None` if the role missing from the cache.
+    """
+
     role: guilds.Role = attr.ib()
     """Role that was updated.
 
diff --git a/hikari/events/user_events.py b/hikari/events/user_events.py
index 749fbaab..6dcb6342 100644
--- a/hikari/events/user_events.py
+++ b/hikari/events/user_events.py
@@ -48,6 +48,12 @@ class OwnUserUpdateEvent(shard_events.ShardEvent):
     shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
     # <<inherited docstring from ShardEvent>>.
 
+    old_user: typing.Optional[users.OwnUser] = attr.ib()
+    """The old application user.
+
+    This will be `builtins.None` if the user missing from the cache.
+    """
+
     user: users.OwnUser = attr.ib()
     """This application user.
 
diff --git a/hikari/events/voice_events.py b/hikari/events/voice_events.py
index b29eecec..7c59ca76 100644
--- a/hikari/events/voice_events.py
+++ b/hikari/events/voice_events.py
@@ -80,6 +80,12 @@ class VoiceStateUpdateEvent(VoiceEvent):
     shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True})
     # <<inherited docstring>>.
 
+    old_state: typing.Optional[voices.VoiceState] = attr.ib(repr=True)
+    """The old voice state.
+
+    This will be `builtins.None` if the voice state missing from the cache.
+    """
+
     state: voices.VoiceState = attr.ib(repr=True)
     """Voice state that this update contained.
 
diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py
index df5230a1..98b33a5b 100644
--- a/hikari/impl/bot.py
+++ b/hikari/impl/bot.py
@@ -26,7 +26,7 @@
 import threading
 import traceback
 
-__all__: typing.List[str] = ["BotApp", "LoggerLevelT"]
+__all__: typing.List[str] = ["BotApp"]
 
 import asyncio
 import concurrent.futures
@@ -72,22 +72,6 @@
     from hikari.api import shard
     from hikari.impl import event_manager_base
 
-LoggerLevelT = typing.Union[
-    int,
-    typing.Literal["TRACE_HIKARI"],
-    typing.Literal["DEBUG"],
-    typing.Literal["INFO"],
-    typing.Literal["WARNING"],
-    typing.Literal["ERROR"],
-    typing.Literal["CRITICAL"],
-]
-"""Type-hint for a valid logging level.
-
-This may be an `int` logging level (e.g. `logging.DEBUG`, `logging.CRITICAL`),
-or a capitalized string that matches one of `"TRACE_HIKARI"`, `"DEBUG"`,
-`"INFO"`, `"WARNING", `"ERROR"`, or `"CRITICAL"`.
-"""
-
 _LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari")
 
 
@@ -148,6 +132,8 @@ class BotApp(traits.BotAware, event_dispatcher.EventDispatcher):
         will __force__ colour to be used in console-based output. Specifying a
         `"CLICOLOR_FORCE"` environment variable with a non-`"0"` value will
         override this setting.
+    cache_settings : typing.Optional[hikari.config.CacheSettings]
+        Optional cache settings. If unspecified, will use the defaults.
     http_settings : typing.Optional[hikari.config.HTTPSettings]
         Optional custom HTTP configuration settings to use. Allows you to
         customise functionality such as whether SSL-verification is enabled,
@@ -267,9 +253,10 @@ def __init__(
         enable_cache: bool = True,
         executor: typing.Optional[concurrent.futures.Executor] = None,
         force_color: bool = False,
+        cache_settings: typing.Optional[config.CacheSettings] = None,
         http_settings: typing.Optional[config.HTTPSettings] = None,
         intents: intents_.Intents = intents_.Intents.ALL_UNPRIVILEGED,
-        logs: typing.Union[None, LoggerLevelT, typing.Dict[str, typing.Any]] = "INFO",
+        logs: typing.Union[None, int, str, typing.Dict[str, typing.Any]] = "INFO",
         max_rate_limit: float = 300,
         proxy_settings: typing.Optional[config.ProxySettings] = None,
         rest_url: typing.Optional[str] = None,
@@ -299,7 +286,7 @@ def __init__(
             from hikari.impl import stateful_event_manager
             from hikari.impl import stateful_guild_chunker
 
-            cache_obj = stateful_cache.StatefulCacheImpl(self, intents)
+            cache_obj = stateful_cache.StatefulCacheImpl(self, intents, cache_settings)
             self._cache = cache_obj
             self._chunker = stateful_guild_chunker.StatefulGuildChunkerImpl(self, chunking_limit)
 
@@ -828,7 +815,7 @@ async def start(
                 continue
             if self._shards:
                 close_waiter = asyncio.create_task(self._closing_event.wait())
-                shard_joiners = [asyncio.ensure_future(s.join()) for s in self._shards.values()]
+                shard_joiners = [s.join() for s in self._shards.values()]
 
                 try:
                     # Attempt to wait for all started shards, for 5 seconds, along with the close
@@ -1023,14 +1010,7 @@ def _validate_activity(activity: undefined.UndefinedNoneOr[presences.Activity])
 
         # If you ever change where this is called from, make sure to check the stacklevels are correct
         # or the code preview in the warning will be wrong...
-        if activity.type is presences.ActivityType.WATCHING:
-            warnings.warn(
-                "The WATCHING activity type is not officially recognised by Discord and may be removed from Hikari in "
-                "a future release.",
-                category=PendingDeprecationWarning,
-                stacklevel=3,
-            )
-        elif activity.type is presences.ActivityType.CUSTOM:
+        if activity.type is presences.ActivityType.CUSTOM:
             warnings.warn(
                 "The CUSTOM activity type is not supported by bots at the time of writing, and may therefore not have "
                 "any effect if used.",
diff --git a/hikari/impl/buckets.py b/hikari/impl/buckets.py
index 5eb16429..a226434f 100644
--- a/hikari/impl/buckets.py
+++ b/hikari/impl/buckets.py
@@ -119,9 +119,6 @@
 the vital rate limit headers manually and parse them to the correct data types.
 These headers are:
 
-* `Date`:
-    the response date on the server. This should be parsed to a
-    `datetime.datetime` using `email.utils.parsedate_to_datetime`.
 * `X-RateLimit-Limit`:
     an `builtins.int` describing the max requests in the bucket from empty to
     being rate limited.
@@ -130,12 +127,9 @@
     limiting occurs in the current window.
 * `X-RateLimit-Bucket`:
     a `builtins.str` containing the initial bucket hash.
-* `X-RateLimit-Reset`:
-    a `builtins.float` containing the number of seconds since
-    1st January 1970 at 0:00:00 UTC at which the current ratelimit window
-    resets. This should be parsed to a `datetime.datetime` using
-    `datetime.datetime.fromtimestamp`, passing `datetime.timezone.utc`
-    as `tz`.
+* `x-ratelimit-reset-after`:
+    a `builtins.float` containing the number of seconds when the current rate
+    limit bucket will reset with decimal millisecond precision.
 
 Each of the above values should be passed to the `update_rate_limits` method to
 ensure that the bucket you acquired time from is correctly updated should
@@ -171,7 +165,7 @@
 No information is sent in headers about these specific limits. You will only
 be made aware that they exist once you get ratelimited. In the 429 ratelimited
 response, you will have the `"global"` attribute set to `builtins.False`, and a
-`"reset_after"` attribute that differs entirely to the `X-RateLimit-Reset`
+`"reset_after"` attribute that differs entirely to the `x-ratelimit-reset-after`
 header. Thus, it is important to not assume the value in the 429 response
 for the reset time is the same as the one in the bucket headers. Hikari's
 `hikari.api.rest.RESTClient` implementation specifically uses the value furthest
@@ -205,7 +199,6 @@
 __all__: typing.List[str] = ["UNKNOWN_HASH", "RESTBucket", "RESTBucketManager"]
 
 import asyncio
-import datetime
 import logging
 import typing
 
@@ -563,8 +556,7 @@ def update_rate_limits(
         bucket_header: str,
         remaining_header: int,
         limit_header: int,
-        date_header: datetime.datetime,
-        reset_at_header: datetime.datetime,
+        reset_after: float,
     ) -> None:
         """Update the rate limits for a bucket using info from a response.
 
@@ -578,16 +570,13 @@ def update_rate_limits(
             The `X-RateLimit-Remaining` header cast to an `builtins.int`.
         limit_header : builtins.int
             The `X-RateLimit-Limit`header cast to an `builtins.int`.
-        date_header : datetime.datetime
-            The `Date` header value as a `datetime.datetime`.
-        reset_at_header : datetime.datetime
-            The `X-RateLimit-Reset` header value as a `datetime.datetime`.
+        reset_after : builtins.float
+            The `x-ratelimit-reset-after` cast to a `builtins.float`.
         """
         self.routes_to_hashes[compiled_route.route] = bucket_header
 
         real_bucket_hash = compiled_route.create_real_bucket_hash(bucket_header)
 
-        reset_after = (reset_at_header - date_header).total_seconds()
         reset_at_monotonic = time.monotonic() + reset_after
 
         if real_bucket_hash in self.real_hashes_to_buckets:
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index 927ff56c..b84d9bde 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -301,7 +301,7 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati
             description=payload["description"],
             is_bot_public=payload.get("bot_public"),
             is_bot_code_grant_required=payload.get("bot_require_code_grant"),
-            owner=self.deserialize_user(payload["owner"]) if "owner" in payload else None,
+            owner=self.deserialize_user(payload["owner"]),
             rpc_origins=payload["rpc_origins"] if "rpc_origins" in payload else None,
             summary=payload["summary"],
             verify_key=bytes(payload["verify_key"], "utf-8") if "verify_key" in payload else None,
@@ -1530,10 +1530,10 @@ def deserialize_partial_message(  # noqa CFQ001 - Function too long
                 primary_sku_id=primary_sku_id,
             )
 
-        message_reference: undefined.UndefinedOr[message_models.MessageCrosspost] = undefined.UNDEFINED
+        message_reference: undefined.UndefinedOr[message_models.MessageReference] = undefined.UNDEFINED
         if "message_reference" in payload:
             crosspost_payload = payload["message_reference"]
-            message_reference = message_models.MessageCrosspost(
+            message_reference = message_models.MessageReference(
                 app=self._app,
                 id=snowflakes.Snowflake(crosspost_payload["message_id"]) if "message_id" in crosspost_payload else None,
                 channel_id=snowflakes.Snowflake(crosspost_payload["channel_id"]),
@@ -1640,13 +1640,13 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model
                 party_id=activity_payload.get("party_id"),
             )
 
-        crosspost: typing.Optional[message_models.MessageCrosspost] = None
+        crosspost: typing.Optional[message_models.MessageReference] = None
         if "message_reference" in payload:
             crosspost_payload = payload["message_reference"]
             crosspost_id = (
                 snowflakes.Snowflake(crosspost_payload["message_id"]) if "message_id" in crosspost_payload else None
             )
-            crosspost = message_models.MessageCrosspost(
+            crosspost = message_models.MessageReference(
                 app=self._app,
                 id=crosspost_id,
                 channel_id=snowflakes.Snowflake(crosspost_payload["channel_id"]),
diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py
index 300f803f..427ab575 100644
--- a/hikari/impl/event_factory.py
+++ b/hikari/impl/event_factory.py
@@ -29,10 +29,15 @@
 import typing
 
 from hikari import channels as channel_models
+from hikari import emojis as emojis_models
+from hikari import guilds as guild_models
+from hikari import messages as messages_models
+from hikari import presences as presences_models
 from hikari import snowflakes
 from hikari import traits
 from hikari import undefined
 from hikari import users as user_models
+from hikari import voices as voices_models
 from hikari.api import event_factory
 from hikari.api import shard as gateway_shard
 from hikari.events import channel_events
@@ -71,11 +76,17 @@ def deserialize_channel_create_event(
         raise TypeError(f"Expected GuildChannel or PrivateChannel but received {type(channel).__name__}")
 
     def deserialize_channel_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_channel: typing.Optional[channel_models.GuildChannel],
     ) -> channel_events.ChannelUpdateEvent:
         channel = self._app.entity_factory.deserialize_channel(payload)
         if isinstance(channel, channel_models.GuildChannel):
-            return channel_events.GuildChannelUpdateEvent(app=self._app, shard=shard, channel=channel)
+            return channel_events.GuildChannelUpdateEvent(
+                app=self._app, shard=shard, channel=channel, old_channel=old_channel
+            )
         if isinstance(channel, channel_models.PrivateChannel):
             raise NotImplementedError("DM channel update events are undocumented behaviour")
         raise TypeError(f"Expected GuildChannel or PrivateChannel but received {type(channel).__name__}")
@@ -196,7 +207,11 @@ def deserialize_guild_create_event(
         )
 
     def deserialize_guild_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_guild: typing.Optional[guild_models.Guild],
     ) -> guild_events.GuildUpdateEvent:
         guild_information = self._app.entity_factory.deserialize_gateway_guild(payload)
         return guild_events.GuildUpdateEvent(
@@ -205,6 +220,7 @@ def deserialize_guild_update_event(
             guild=guild_information.guild,
             emojis=guild_information.emojis,
             roles=guild_information.roles,
+            old_guild=old_guild,
         )
 
     def deserialize_guild_leave_event(
@@ -240,14 +256,20 @@ def deserialize_guild_ban_remove_event(
         )
 
     def deserialize_guild_emojis_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_emojis: typing.Optional[typing.Sequence[emojis_models.KnownCustomEmoji]],
     ) -> guild_events.EmojisUpdateEvent:
         guild_id = snowflakes.Snowflake(payload["guild_id"])
         emojis = [
             self._app.entity_factory.deserialize_known_custom_emoji(emoji, guild_id=guild_id)
             for emoji in payload["emojis"]
         ]
-        return guild_events.EmojisUpdateEvent(app=self._app, shard=shard, guild_id=guild_id, emojis=emojis)
+        return guild_events.EmojisUpdateEvent(
+            app=self._app, shard=shard, guild_id=guild_id, emojis=emojis, old_emojis=old_emojis
+        )
 
     def deserialize_integration_create_event(
         self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
@@ -285,10 +307,14 @@ def deserialize_guild_member_add_event(
         return member_events.MemberCreateEvent(app=self._app, shard=shard, member=member)
 
     def deserialize_guild_member_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_member: typing.Optional[guild_models.Member],
     ) -> member_events.MemberUpdateEvent:
         member = self._app.entity_factory.deserialize_member(payload)
-        return member_events.MemberUpdateEvent(app=self._app, shard=shard, member=member)
+        return member_events.MemberUpdateEvent(app=self._app, shard=shard, member=member, old_member=old_member)
 
     def deserialize_guild_member_remove_event(
         self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
@@ -307,13 +333,17 @@ def deserialize_guild_role_create_event(
         return role_events.RoleCreateEvent(app=self._app, shard=shard, role=role)
 
     def deserialize_guild_role_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_role: typing.Optional[guild_models.Role],
     ) -> role_events.RoleUpdateEvent:
         role = self._app.entity_factory.deserialize_role(
             payload["role"],
             guild_id=snowflakes.Snowflake(payload["guild_id"]),
         )
-        return role_events.RoleUpdateEvent(app=self._app, shard=shard, role=role)
+        return role_events.RoleUpdateEvent(app=self._app, shard=shard, role=role, old_role=old_role)
 
     def deserialize_guild_role_delete_event(
         self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
@@ -328,7 +358,11 @@ def deserialize_guild_role_delete_event(
     # TODO: fix test case for this method. I managed to indent the return into the
     # `if(user_payload) > 1` without any tests failing!
     def deserialize_presence_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_presence: typing.Optional[presences_models.MemberPresence],
     ) -> guild_events.PresenceUpdateEvent:
         presence = self._app.entity_factory.deserialize_member_presence(payload)
 
@@ -353,7 +387,9 @@ def deserialize_presence_update_event(
                 is_system=user_payload.get("system", undefined.UNDEFINED),
                 flags=flags,
             )
-        return guild_events.PresenceUpdateEvent(app=self._app, shard=shard, presence=presence, user=user)
+        return guild_events.PresenceUpdateEvent(
+            app=self._app, shard=shard, presence=presence, user=user, old_presence=old_presence
+        )
 
     ##################
     # MESSAGE EVENTS #
@@ -370,14 +406,22 @@ def deserialize_message_create_event(
         return message_events.GuildMessageCreateEvent(app=self._app, shard=shard, message=message)
 
     def deserialize_message_update_event(  # noqa: CFQ001
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_message: typing.Optional[messages_models.PartialMessage],
     ) -> message_events.MessageUpdateEvent:
         message = self._app.entity_factory.deserialize_partial_message(payload)
 
         if message.guild_id is None:
-            return message_events.DMMessageUpdateEvent(app=self._app, shard=shard, message=message)
+            return message_events.DMMessageUpdateEvent(
+                app=self._app, shard=shard, message=message, old_message=old_message
+            )
 
-        return message_events.GuildMessageUpdateEvent(app=self._app, shard=shard, message=message)
+        return message_events.GuildMessageUpdateEvent(
+            app=self._app, shard=shard, message=message, old_message=old_message
+        )
 
     def deserialize_message_delete_event(
         self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
@@ -550,10 +594,14 @@ def deserialize_ready_event(
         )
 
     def deserialize_own_user_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_user: typing.Optional[user_models.OwnUser],
     ) -> user_events.OwnUserUpdateEvent:
         return user_events.OwnUserUpdateEvent(
-            app=self._app, shard=shard, user=self._app.entity_factory.deserialize_my_user(payload)
+            app=self._app, shard=shard, user=self._app.entity_factory.deserialize_my_user(payload), old_user=old_user
         )
 
     def deserialize_guild_member_chunk_event(
@@ -598,10 +646,14 @@ def deserialize_guild_member_chunk_event(
     ################
 
     def deserialize_voice_state_update_event(
-        self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
+        self,
+        shard: gateway_shard.GatewayShard,
+        payload: data_binding.JSONObject,
+        *,
+        old_state: typing.Optional[voices_models.VoiceState],
     ) -> voice_events.VoiceStateUpdateEvent:
         state = self._app.entity_factory.deserialize_voice_state(payload)
-        return voice_events.VoiceStateUpdateEvent(app=self._app, shard=shard, state=state)
+        return voice_events.VoiceStateUpdateEvent(app=self._app, shard=shard, state=state, old_state=old_state)
 
     def deserialize_voice_server_update_event(
         self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index 042095ff..b7d00485 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -654,18 +654,14 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response
         limit = int(resp_headers.get(_X_RATELIMIT_LIMIT_HEADER, "1"))
         remaining = int(resp_headers.get(_X_RATELIMIT_REMAINING_HEADER, "1"))
         bucket = resp_headers.get(_X_RATELIMIT_BUCKET_HEADER, "None")
-        reset_at = float(resp_headers.get(_X_RATELIMIT_RESET_HEADER, "0"))
         reset_after = float(resp_headers.get(_X_RATELIMIT_RESET_AFTER_HEADER, "0"))
-        reset_date = datetime.datetime.fromtimestamp(reset_at, tz=datetime.timezone.utc)
-        now_date = time.rfc7231_datetime_string_to_datetime(resp_headers[_DATE_HEADER])
 
         self.buckets.update_rate_limits(
             compiled_route=compiled_route,
             bucket_header=bucket,
             remaining_header=remaining,
             limit_header=limit,
-            date_header=now_date,
-            reset_at_header=reset_date,
+            reset_after=reset_after,
         )
 
         if response.status != http.HTTPStatus.TOO_MANY_REQUESTS:
diff --git a/hikari/impl/stateful_cache.py b/hikari/impl/stateful_cache.py
index 779d7a9c..bea45225 100644
--- a/hikari/impl/stateful_cache.py
+++ b/hikari/impl/stateful_cache.py
@@ -30,11 +30,13 @@
 import typing
 
 from hikari import channels
+from hikari import config
 from hikari import emojis
 from hikari import errors
 from hikari import guilds
 from hikari import intents as intents_
 from hikari import invites
+from hikari import messages
 from hikari import presences
 from hikari import snowflakes
 from hikari import undefined
@@ -96,6 +98,7 @@ class StatefulCacheImpl(cache.MutableCache):
         "_role_entries",
         "_unknown_custom_emoji_entries",
         "_user_entries",
+        "_message_entries",
     )
 
     # For the sake of keeping things clean, the annotations are being kept separate from the assignment here.
@@ -111,9 +114,15 @@ class StatefulCacheImpl(cache.MutableCache):
         cache_utility.GenericRefWrapper[emojis.CustomEmoji],
     ]
     _user_entries: collections.ExtendedMutableMapping[snowflakes.Snowflake, cache_utility.GenericRefWrapper[users.User]]
+    _message_entries: collections.LimitedCapacityCacheMap[snowflakes.Snowflake, messages.Message]
     _intents: intents_.Intents
 
-    def __init__(self, app: traits.RESTAware, intents: intents_.Intents) -> None:
+    def __init__(
+        self, app: traits.RESTAware, intents: intents_.Intents, settings: typing.Optional[config.CacheSettings]
+    ) -> None:
+        if settings is None:
+            settings = config.CacheSettings()
+
         self._app = app
         self._me = None
         self._emoji_entries = collections.FreezableDict()
@@ -125,6 +134,7 @@ def __init__(self, app: traits.RESTAware, intents: intents_.Intents) -> None:
         # found attached to cached presence activities.
         self._unknown_custom_emoji_entries = collections.FreezableDict()
         self._user_entries = collections.FreezableDict()
+        self._message_entries = collections.LimitedCapacityCacheMap(limit=settings.max_messages)
         self._intents = intents
 
     def _assert_has_intent(self, intents: intents_.Intents, /) -> None:
@@ -680,7 +690,6 @@ def get_invites_view_for_channel(
 
         cached_invites = {}
         cached_users = {}
-        invite_ids: typing.Iterable[str]
 
         # Tuple casts like this avoids edge case issues which would be caused by arrays being modified while we're
         # iterating over them.
@@ -1427,7 +1436,7 @@ def clear_voice_states_for_channel(
 
         for user_id, voice_state in guild_record.voice_states.items():
             if voice_state.channel_id == channel_id:
-                cached_voice_states[voice_state.user_id] = voice_state
+                cached_voice_states[user_id] = voice_state
                 self._chainable_remove_voice_state_assets(voice_state, guild_record, cached_members, cached_users)
 
         if not guild_record.voice_states:
@@ -1598,3 +1607,57 @@ def update_voice_state(
         cached_voice_state = self.get_voice_state(voice_state.guild_id, voice_state.user_id)
         self.set_voice_state(voice_state)
         return cached_voice_state, self.get_voice_state(voice_state.guild_id, voice_state.user_id)
+
+    def clear_messages(self) -> cache.CacheView[snowflakes.Snowflake, messages.Message]:
+        if not self._message_entries:
+            return cache_utility.EmptyCacheView()
+
+        cached_messages = self._message_entries.copy()
+        self._message_entries.clear()
+
+        return cache_utility.StatefulCacheMappingView(cached_messages)
+
+    def delete_message(self, message_id: snowflakes.Snowflake, /) -> typing.Optional[messages.Message]:
+        return self._message_entries.pop(message_id) if message_id in self._message_entries else None
+
+    def get_message(self, message_id: snowflakes.Snowflake, /) -> typing.Optional[messages.Message]:
+        message = self._message_entries.get(message_id)
+        if message:
+            message = copy.copy(message)
+            message.author = copy.copy(message.author)
+
+        return message
+
+    def get_messages_view(self) -> cache.CacheView[snowflakes.Snowflake, messages.Message]:
+        cached_messages = self._message_entries.freeze()
+        return cache_utility.StatefulCacheMappingView(cached_messages)
+
+    def set_message(self, message: messages.Message, /) -> None:
+        self._message_entries[message.id] = copy.copy(message)
+
+    def update_message(
+        self, message: typing.Union[messages.PartialMessage, messages.Message], /
+    ) -> typing.Tuple[typing.Optional[messages.Message], typing.Optional[messages.Message]]:
+        cached_message = self.get_message(message.id)
+
+        if isinstance(message, messages.Message):
+            self.set_message(message)
+        else:
+            copied_message = copy.copy(cached_message)
+            if copied_message:
+                keys = [
+                    "content",
+                    "edited_timestamp",
+                    "mentions",
+                    "embeds",
+                    "is_pinned",
+                ]
+
+                for key in keys:
+                    new_value = getattr(message, key)
+                    if new_value is not undefined.UNDEFINED:
+                        setattr(copied_message, key, new_value)
+
+                self.set_message(copied_message)
+
+        return cached_message, self.get_message(message.id)
diff --git a/hikari/impl/stateful_event_manager.py b/hikari/impl/stateful_event_manager.py
index ec0c1bd0..54322bc2 100644
--- a/hikari/impl/stateful_event_manager.py
+++ b/hikari/impl/stateful_event_manager.py
@@ -31,6 +31,7 @@
 from hikari import channels
 from hikari import intents as intents_
 from hikari import presences
+from hikari import snowflakes
 from hikari import traits
 from hikari.events import shard_events
 from hikari.impl import event_manager_base
@@ -95,7 +96,8 @@ async def on_channel_create(self, shard: gateway_shard.GatewayShard, payload: da
 
     async def on_channel_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#channel-update for more info."""
-        event = self._app.event_factory.deserialize_channel_update_event(shard, payload)
+        old = self._cache.get_guild_channel(snowflakes.Snowflake(payload["id"]))
+        event = self._app.event_factory.deserialize_channel_update_event(shard, payload, old_channel=old)
         assert isinstance(event.channel, channels.GuildChannel), "channel update events for DM channels are unexpected"
         self._cache.update_guild_channel(event.channel)
         await self.dispatch(event)
@@ -156,7 +158,8 @@ async def on_guild_create(self, shard: gateway_shard.GatewayShard, payload: data
 
     async def on_guild_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#guild-update for more info."""
-        event = self._app.event_factory.deserialize_guild_update_event(shard, payload)
+        old = self._cache.get_guild(snowflakes.Snowflake(payload["id"]))
+        event = self._app.event_factory.deserialize_guild_update_event(shard, payload, old_guild=old)
         self._cache.update_guild(event.guild)
 
         self._cache.clear_roles_for_guild(event.guild.id)
@@ -200,7 +203,10 @@ async def on_guild_ban_remove(self, shard: gateway_shard.GatewayShard, payload:
 
     async def on_guild_emojis_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#guild-emojis-update for more info."""
-        event = self._app.event_factory.deserialize_guild_emojis_update_event(shard, payload)
+        emojis_view = self._cache.get_emojis_view_for_guild(snowflakes.Snowflake(payload["guild_id"]))
+        old = list(emojis_view.values())
+
+        event = self._app.event_factory.deserialize_guild_emojis_update_event(shard, payload, old_emojis=old)
         self._cache.clear_emojis_for_guild(event.guild_id)
 
         for emoji in event.emojis:
@@ -237,7 +243,10 @@ async def on_guild_member_remove(self, shard: gateway_shard.GatewayShard, payloa
 
     async def on_guild_member_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#guild-member-update for more info."""
-        event = self._app.event_factory.deserialize_guild_member_update_event(shard, payload)
+        old = self._cache.get_member(
+            snowflakes.Snowflake(payload["guild_id"]), snowflakes.Snowflake(payload["user"]["id"])
+        )
+        event = self._app.event_factory.deserialize_guild_member_update_event(shard, payload, old_member=old)
         self._cache.update_member(event.member)
         await self.dispatch(event)
 
@@ -262,7 +271,8 @@ async def on_guild_role_create(self, shard: gateway_shard.GatewayShard, payload:
 
     async def on_guild_role_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#guild-role-update for more info."""
-        event = self._app.event_factory.deserialize_guild_role_update_event(shard, payload)
+        old = self._cache.get_role(snowflakes.Snowflake(payload["role"]["id"]))
+        event = self._app.event_factory.deserialize_guild_role_update_event(shard, payload, old_role=old)
         self._cache.update_role(event.role)
         await self.dispatch(event)
 
@@ -286,21 +296,28 @@ async def on_invite_delete(self, shard: gateway_shard.GatewayShard, payload: dat
 
     async def on_message_create(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#message-create for more info."""
-        await self.dispatch(self._app.event_factory.deserialize_message_create_event(shard, payload))
-
-    # TODO: message cache.
+        event = self._app.event_factory.deserialize_message_create_event(shard, payload)
+        self._cache.set_message(event.message)
+        await self.dispatch(event)
 
     async def on_message_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#message-update for more info."""
-        await self.dispatch(self._app.event_factory.deserialize_message_update_event(shard, payload))
+        old = self._cache.get_message(snowflakes.Snowflake(payload["id"]))
+        event = self._app.event_factory.deserialize_message_update_event(shard, payload, old_message=old)
+        self._cache.update_message(event.message)
+        await self.dispatch(event)
 
     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."""
-        await self.dispatch(self._app.event_factory.deserialize_message_delete_event(shard, payload))
+        event = self._app.event_factory.deserialize_message_delete_event(shard, payload)
+        self._cache.delete_message(event.message_id)
+        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."""
-        await self.dispatch(self._app.event_factory.deserialize_message_delete_bulk_event(shard, payload))
+        event = self._app.event_factory.deserialize_message_delete_bulk_event(shard, payload)
+        [self._cache.delete_message(_id) for _id in event.message_ids]
+        await self.dispatch(event)
 
     async def on_message_reaction_add(
         self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
@@ -330,7 +347,10 @@ async def on_message_reaction_remove_emoji(
 
     async def on_presence_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#presence-update for more info."""
-        event = self._app.event_factory.deserialize_presence_update_event(shard, payload)
+        old = self._cache.get_presence(
+            snowflakes.Snowflake(payload["guild_id"]), snowflakes.Snowflake(payload["user"]["id"])
+        )
+        event = self._app.event_factory.deserialize_presence_update_event(shard, payload, old_presence=old)
 
         if event.presence.visible_status is presences.Status.OFFLINE:
             self._cache.delete_presence(event.presence.guild_id, event.presence.user_id)
@@ -346,13 +366,17 @@ async def on_typing_start(self, shard: gateway_shard.GatewayShard, payload: data
 
     async def on_user_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#user-update for more info."""
-        event = self._app.event_factory.deserialize_own_user_update_event(shard, payload)
+        old = self._cache.get_me()
+        event = self._app.event_factory.deserialize_own_user_update_event(shard, payload, old_user=old)
         self._cache.update_me(event.user)
         await self.dispatch(event)
 
     async def on_voice_state_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#voice-state-update for more info."""
-        event = self._app.event_factory.deserialize_voice_state_update_event(shard, payload)
+        old = self._cache.get_voice_state(
+            snowflakes.Snowflake(payload["guild_id"]), snowflakes.Snowflake(payload["user_id"])
+        )
+        event = self._app.event_factory.deserialize_voice_state_update_event(shard, payload, old_state=old)
 
         if event.state.channel_id is None:
             self._cache.delete_voice_state(event.state.guild_id, event.state.user_id)
diff --git a/hikari/impl/stateless_cache.py b/hikari/impl/stateless_cache.py
index 32894d10..f3b0209e 100644
--- a/hikari/impl/stateless_cache.py
+++ b/hikari/impl/stateless_cache.py
@@ -38,6 +38,7 @@
     from hikari import emojis
     from hikari import guilds
     from hikari import invites
+    from hikari import messages
     from hikari import presences
     from hikari import snowflakes
     from hikari import users
@@ -390,3 +391,23 @@ def update_voice_state(
         self, voice_state: voices.VoiceState, /
     ) -> typing.Tuple[typing.Optional[voices.VoiceState], typing.Optional[voices.VoiceState]]:
         raise self._no_cache()
+
+    def clear_messages(self) -> cache.CacheView[snowflakes.Snowflake, messages.Message]:
+        raise self._no_cache()
+
+    def delete_message(self, message_id: snowflakes.Snowflake, /) -> typing.Optional[messages.Message]:
+        raise self._no_cache()
+
+    def get_message(self, message_id: snowflakes.Snowflake, /) -> typing.Optional[messages.Message]:
+        return None
+
+    def get_messages_view(self) -> cache.CacheView[snowflakes.Snowflake, messages.Message]:
+        return cache_utilities.EmptyCacheView()
+
+    def set_message(self, message: messages.Message, /) -> None:
+        raise self._no_cache()
+
+    def update_message(
+        self, message: typing.Union[messages.PartialMessage, messages.Message], /
+    ) -> typing.Tuple[typing.Optional[messages.Message], typing.Optional[messages.Message]]:
+        raise self._no_cache()
diff --git a/hikari/impl/stateless_event_manager.py b/hikari/impl/stateless_event_manager.py
index 884757b1..25910bf2 100644
--- a/hikari/impl/stateless_event_manager.py
+++ b/hikari/impl/stateless_event_manager.py
@@ -77,7 +77,7 @@ async def on_channel_create(self, shard: gateway_shard.GatewayShard, payload: da
 
     async def on_channel_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#channel-update for more info."""
-        await self.dispatch(self._app.event_factory.deserialize_channel_update_event(shard, payload))
+        await self.dispatch(self._app.event_factory.deserialize_channel_update_event(shard, payload, old_channel=None))
 
     async def on_channel_delete(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#channel-delete for more info."""
@@ -93,7 +93,7 @@ async def on_guild_create(self, shard: gateway_shard.GatewayShard, payload: data
 
     async def on_guild_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#guild-update for more info."""
-        await self.dispatch(self._app.event_factory.deserialize_guild_update_event(shard, payload))
+        await self.dispatch(self._app.event_factory.deserialize_guild_update_event(shard, payload, old_guild=None))
 
     async def on_guild_delete(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#guild-delete for more info."""
@@ -112,7 +112,9 @@ async def on_guild_ban_remove(self, shard: gateway_shard.GatewayShard, payload:
 
     async def on_guild_emojis_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#guild-emojis-update for more info."""
-        await self.dispatch(self._app.event_factory.deserialize_guild_emojis_update_event(shard, payload))
+        await self.dispatch(
+            self._app.event_factory.deserialize_guild_emojis_update_event(shard, payload, old_emojis=None)
+        )
 
     async def on_integration_create(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         await self.dispatch(self._app.event_factory.deserialize_integration_create_event(shard, payload))
@@ -133,7 +135,9 @@ async def on_guild_member_remove(self, shard: gateway_shard.GatewayShard, payloa
 
     async def on_guild_member_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#guild-member-update for more info."""
-        await self.dispatch(self._app.event_factory.deserialize_guild_member_update_event(shard, payload))
+        await self.dispatch(
+            self._app.event_factory.deserialize_guild_member_update_event(shard, payload, old_member=None)
+        )
 
     async def on_guild_members_chunk(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#guild-members-chunk for more info."""
@@ -145,7 +149,7 @@ async def on_guild_role_create(self, shard: gateway_shard.GatewayShard, payload:
 
     async def on_guild_role_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#guild-role-update for more info."""
-        await self.dispatch(self._app.event_factory.deserialize_guild_role_update_event(shard, payload))
+        await self.dispatch(self._app.event_factory.deserialize_guild_role_update_event(shard, payload, old_role=None))
 
     async def on_guild_role_delete(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#guild-role-delete for more info."""
@@ -165,7 +169,7 @@ async def on_message_create(self, shard: gateway_shard.GatewayShard, payload: da
 
     async def on_message_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#message-update for more info."""
-        await self.dispatch(self._app.event_factory.deserialize_message_update_event(shard, payload))
+        await self.dispatch(self._app.event_factory.deserialize_message_update_event(shard, payload, old_message=None))
 
     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."""
@@ -201,7 +205,9 @@ async def on_message_reaction_remove_emoji(
 
     async def on_presence_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#presence-update for more info."""
-        await self.dispatch(self._app.event_factory.deserialize_presence_update_event(shard, payload))
+        await self.dispatch(
+            self._app.event_factory.deserialize_presence_update_event(shard, payload, old_presence=None)
+        )
 
     async def on_typing_start(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#typing-start for more info."""
@@ -209,11 +215,13 @@ async def on_typing_start(self, shard: gateway_shard.GatewayShard, payload: data
 
     async def on_user_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#user-update for more info."""
-        await self.dispatch(self._app.event_factory.deserialize_own_user_update_event(shard, payload))
+        await self.dispatch(self._app.event_factory.deserialize_own_user_update_event(shard, payload, old_user=None))
 
     async def on_voice_state_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#voice-state-update for more info."""
-        await self.dispatch(self._app.event_factory.deserialize_voice_state_update_event(shard, payload))
+        await self.dispatch(
+            self._app.event_factory.deserialize_voice_state_update_event(shard, payload, old_state=None)
+        )
 
     async def on_voice_server_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
         """See https://discord.com/developers/docs/topics/gateway#voice-server-update for more info."""
diff --git a/hikari/internal/data_binding.py b/hikari/internal/data_binding.py
index cb55f33a..8e81ac5c 100644
--- a/hikari/internal/data_binding.py
+++ b/hikari/internal/data_binding.py
@@ -94,7 +94,7 @@ class StringMapBuilder(multidict.MultiDict[str]):
     the amount of boilerplate needed for generating the headers and query strings for
     low-level HTTP API interaction, amongst other things.
 
-    !!! warn
+    !!! warning
         Because this subclasses `builtins.dict`, you should not use the
         index operator to set items on this object. Doing so will skip any
         form of validation on the type. Use the `put*` methods instead.
@@ -163,7 +163,7 @@ class JSONObjectBuilder(typing.Dict[str, JSONish]):
     This speeds up generation of JSON payloads for low level HTTP and websocket
     API interaction.
 
-    !!! warn
+    !!! warning
         Because this subclasses `builtins.dict`, you should not use the
         index operator to set items on this object. Doing so will skip any
         form of validation on the type. Use the `put*` methods instead.
diff --git a/hikari/internal/time.py b/hikari/internal/time.py
index c52e49d6..6d441a5a 100644
--- a/hikari/internal/time.py
+++ b/hikari/internal/time.py
@@ -25,7 +25,6 @@
 
 __all__: typing.List[str] = [
     "DISCORD_EPOCH",
-    "rfc7231_datetime_string_to_datetime",
     "datetime_to_discord_epoch",
     "discord_epoch_to_datetime",
     "unix_epoch_to_datetime",
@@ -39,7 +38,6 @@
 ]
 
 import datetime
-import email.utils
 import time
 import typing
 import uuid as uuid_
@@ -67,28 +65,6 @@
 _ISO_8601_FORMAT: typing.Final[str] = "%Y-%m-%dT%H:%M:%s"
 
 
-def rfc7231_datetime_string_to_datetime(date_str: str, /) -> datetime.datetime:
-    """Return the HTTP date as a datetime object.
-
-    Parameters
-    ----------
-    date_str : builtins.str
-        The RFC-2822 (section 3.3) compliant date string to parse.
-
-    Returns
-    -------
-    datetime.datetime
-        The HTTP date as a datetime object.
-
-    References
-    ----------
-    * [RFC-2822](https://www.ietf.org/rfc/rfc2822.txt)
-    * [Mozilla documentation for `Date` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date)
-    """
-    # According to Mozilla, these are always going to be GMT (which is UTC).
-    return email.utils.parsedate_to_datetime(date_str).replace(tzinfo=datetime.timezone.utc)
-
-
 # Default to the standard lib parser, that isn't really ISO compliant but seems
 # to work for what we need.
 def iso8601_datetime_string_to_datetime(datetime_str: str) -> datetime.datetime:
diff --git a/hikari/internal/ux.py b/hikari/internal/ux.py
index 7cf300d6..2ce3b779 100644
--- a/hikari/internal/ux.py
+++ b/hikari/internal/ux.py
@@ -127,7 +127,8 @@ def init_logging(
             format="%(levelname)-1.1s %(asctime)23.23s %(name)s: %(message)s",
             stream=sys.stderr,
         )
-        logging.captureWarnings(True)
+
+    logging.captureWarnings(True)
 
 
 def print_banner(package: typing.Optional[str], allow_color: bool, force_color: bool) -> None:
diff --git a/hikari/messages.py b/hikari/messages.py
index 83774099..ddd1a03c 100644
--- a/hikari/messages.py
+++ b/hikari/messages.py
@@ -31,7 +31,7 @@
     "Reaction",
     "MessageActivity",
     "Mentions",
-    "MessageCrosspost",
+    "MessageReference",
     "PartialMessage",
     "Message",
 ]
@@ -345,36 +345,30 @@ def _map_cache_maybe_discover(
 
 @attr_extensions.with_copy
 @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False)
-class MessageCrosspost:
-    """Represents information about a cross-posted message.
+class MessageReference:
+    """Represents information about a referenced message.
 
-    This is a message that is sent in one channel/guild and may be
-    "published" to another.
+    This will be included in crossposted messages, channel follow add
+    message, pin add messages and replies.
     """
 
     app: traits.RESTAware = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True})
     """The client application that models may use for procedures."""
 
-    # TODO: get clarification on this! If it cannot happen, this should subclass PartialMessage too.
     id: typing.Optional[snowflakes.Snowflake] = attr.ib(repr=True)
-    """The ID of the message.
+    """The ID of the original message.
 
-    !!! warning
-        This may be `builtins.None` in some cases according to the Discord API
-        documentation, but the situations that cause this to occur are not
-        currently documented.
+    This will be `builtins.None` for channel follow add messages.
     """
 
     channel_id: snowflakes.Snowflake = attr.ib(repr=True)
-    """The ID of the channel that the message originated from."""
+    """The ID of the channel that the original message originated from."""
 
     guild_id: typing.Optional[snowflakes.Snowflake] = attr.ib(repr=True)
     """The ID of the guild that the message originated from.
 
-    !!! warning
-        This may be `builtins.None` in some cases according to the Discord API
-        documentation, but the situations that cause this to occur are not
-        currently documented.
+    This will be `builtins.None` when the original message is not from
+    a guild.
     """
 
 
@@ -444,7 +438,7 @@ class PartialMessage(snowflakes.Unique):
     `MessageUpdateEvent`, but for all other purposes should be treated as
     being optionally specified.
 
-    !!! warn
+    !!! warning
         All fields on this model except `channel` and `id` may be set to
         `hikari.undefined.UNDEFINED` (a singleton) if we have not
         received information about their state from Discord alongside field
@@ -510,10 +504,10 @@ class PartialMessage(snowflakes.Unique):
     """The message attachments."""
 
     embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]] = attr.ib(repr=False)
-    """The message's embeds."""
+    """The message embeds."""
 
     reactions: undefined.UndefinedOr[typing.Sequence[Reaction]] = attr.ib(repr=False)
-    """The message's reactions."""
+    """The message reactions."""
 
     is_pinned: undefined.UndefinedOr[bool] = attr.ib(repr=False)
     """Whether the message is pinned."""
@@ -522,19 +516,19 @@ class PartialMessage(snowflakes.Unique):
     """If the message was generated by a webhook, the webhook's ID."""
 
     type: undefined.UndefinedOr[typing.Union[MessageType, int]] = attr.ib(repr=False)
-    """The message's type."""
+    """The message type."""
 
     activity: undefined.UndefinedNoneOr[MessageActivity] = attr.ib(repr=False)
-    """The message's activity."""
+    """The message activity."""
 
     application: undefined.UndefinedNoneOr[MessageApplication] = attr.ib(repr=False)
-    """The message's application."""
+    """The message application."""
 
-    message_reference: undefined.UndefinedNoneOr[MessageCrosspost] = attr.ib(repr=False)
-    """The message's cross-posted reference data."""
+    message_reference: undefined.UndefinedNoneOr[MessageReference] = attr.ib(repr=False)
+    """The message reference data."""
 
     flags: undefined.UndefinedNoneOr[MessageFlag] = attr.ib(repr=False)
-    """The message's flags."""
+    """The message flags."""
 
     nonce: undefined.UndefinedNoneOr[str] = attr.ib(repr=False)
     """The message nonce.
@@ -1081,8 +1075,8 @@ class Message(PartialMessage):
     application: typing.Optional[MessageApplication]
     """The message application."""
 
-    message_reference: typing.Optional[MessageCrosspost]
-    """The message crossposted reference data."""
+    message_reference: typing.Optional[MessageReference]
+    """The message reference data."""
 
     flags: typing.Optional[MessageFlag]
     """The message flags."""


Test patch:
diff --git a/tests/hikari/events/test_channel_events.py b/tests/hikari/events/test_channel_events.py
index 40f3e0cb..642e6890 100644
--- a/tests/hikari/events/test_channel_events.py
+++ b/tests/hikari/events/test_channel_events.py
@@ -110,12 +110,18 @@ def test_channel_id_property(self, event):
 class TestGuildChannelUpdateEvent:
     @pytest.fixture()
     def event(self):
-        return channel_events.GuildChannelUpdateEvent(app=None, channel=mock.Mock(), shard=None)
+        return channel_events.GuildChannelUpdateEvent(
+            app=None, channel=mock.Mock(), old_channel=mock.Mock(), shard=None
+        )
 
     def test_guild_id_property(self, event):
         event.channel.guild_id = 123
         assert event.guild_id == 123
 
+    def test_old_channel_id_property(self, event):
+        event.old_channel.id = 123
+        assert event.old_channel.id == 123
+
 
 class TestChannelDeleteEvent:
     @pytest.fixture()
diff --git a/tests/hikari/events/test_guild_events.py b/tests/hikari/events/test_guild_events.py
index 2623d690..385892f6 100644
--- a/tests/hikari/events/test_guild_events.py
+++ b/tests/hikari/events/test_guild_events.py
@@ -93,19 +93,32 @@ class TestGuildUpdateEvent:
     @pytest.fixture()
     def event(self):
         return guild_events.GuildUpdateEvent(
-            app=None, shard=object(), guild=mock.Mock(guilds.Guild), emojis={}, roles={}
+            app=None,
+            shard=object(),
+            guild=mock.Mock(guilds.Guild),
+            old_guild=mock.Mock(guilds.Guild),
+            emojis={},
+            roles={},
         )
 
     def test_guild_id_property(self, event):
         event.guild.id = 123
         assert event.guild_id == 123
 
+    def test_old_guild_id_property(self, event):
+        event.old_guild.id = 123
+        assert event.old_guild.id == 123
+
 
 class TestPresenceUpdateEvent:
     @pytest.fixture()
     def event(self):
         return guild_events.PresenceUpdateEvent(
-            app=None, shard=object(), presence=mock.Mock(presences.MemberPresence), user=mock.Mock()
+            app=None,
+            shard=object(),
+            presence=mock.Mock(presences.MemberPresence),
+            old_presence=mock.Mock(presences.MemberPresence),
+            user=mock.Mock(),
         )
 
     def test_user_id_property(self, event):
@@ -115,3 +128,10 @@ def test_user_id_property(self, event):
     def test_guild_id_property(self, event):
         event.presence.guild_id = 123
         assert event.guild_id == 123
+
+    def test_old_presence(self, event):
+        event.old_presence.id = 123
+        event.old_presence.guild_id = 456
+
+        assert event.old_presence.id == 123
+        assert event.old_presence.guild_id == 456
diff --git a/tests/hikari/events/test_member_events.py b/tests/hikari/events/test_member_events.py
index b29ec85e..f20f00b5 100644
--- a/tests/hikari/events/test_member_events.py
+++ b/tests/hikari/events/test_member_events.py
@@ -22,6 +22,7 @@
 import mock
 import pytest
 
+from hikari import guilds
 from hikari import snowflakes
 from hikari.events import member_events
 from tests.hikari import hikari_test_helpers
@@ -74,7 +75,9 @@ def test_user_property(self, event):
 class TestMemberUpdateEvent:
     @pytest.fixture()
     def event(self):
-        return member_events.MemberUpdateEvent(app=None, shard=None, member=mock.Mock())
+        return member_events.MemberUpdateEvent(
+            app=None, shard=None, member=mock.Mock(), old_member=mock.Mock(guilds.Member)
+        )
 
     def test_guild_property(self, event):
         event.member.guild_id = 123
@@ -84,3 +87,10 @@ def test_user_property(self, event):
         user = object()
         event.member.user = user
         event.user == user
+
+    def test_old_user_property(self, event):
+        event.member.guild_id = 123
+        event.member.id = 456
+
+        assert event.member.guild_id == 123
+        assert event.member.id == 456
diff --git a/tests/hikari/events/test_message_events.py b/tests/hikari/events/test_message_events.py
index ea43bbc1..dafdeafc 100644
--- a/tests/hikari/events/test_message_events.py
+++ b/tests/hikari/events/test_message_events.py
@@ -209,6 +209,7 @@ def event(self):
                 guild_id=snowflakes.Snowflake(54123123123),
                 channel_id=snowflakes.Snowflake(800001066),
             ),
+            old_message=mock.Mock(messages.Message, id=123),
             shard=mock.Mock(),
         )
 
@@ -261,6 +262,9 @@ def test_guild_property(self, event):
         assert result is event.app.cache.get_guild.return_value
         event.app.cache.get_guild.assert_called_once_with(54123123123)
 
+    def test_old_message(self, event):
+        assert event.old_message.id == 123
+
 
 class TestDMMessageUpdateEvent:
     @pytest.fixture()
@@ -270,9 +274,13 @@ def event(self):
             message=mock.Mock(
                 spec_set=messages.Message, author=mock.Mock(spec_set=users.User, id=snowflakes.Snowflake(8000010662))
             ),
+            old_message=mock.Mock(messages.Message, id=123),
             shard=mock.Mock(),
         )
 
+    def test_old_message(self, event):
+        assert event.old_message.id == 123
+
 
 class TestMessageDeleteEvent:
     def test_message_id_property(self):
diff --git a/tests/hikari/events/test_role_events.py b/tests/hikari/events/test_role_events.py
index 373a50f1..11f662a7 100644
--- a/tests/hikari/events/test_role_events.py
+++ b/tests/hikari/events/test_role_events.py
@@ -43,7 +43,9 @@ def test_role_id_property(self, event):
 class TestRoleUpdateEvent:
     @pytest.fixture()
     def event(self):
-        return role_events.RoleUpdateEvent(app=None, shard=object(), role=mock.Mock(guilds.Role))
+        return role_events.RoleUpdateEvent(
+            app=None, shard=object(), role=mock.Mock(guilds.Role), old_role=mock.Mock(guilds.Role)
+        )
 
     def test_guild_id_property(self, event):
         event.role.guild_id = 123
@@ -52,3 +54,10 @@ def test_guild_id_property(self, event):
     def test_role_id_property(self, event):
         event.role.id = 123
         assert event.role_id == 123
+
+    def test_old_role(self, event):
+        event.old_role.guild_id = 123
+        event.old_role.id = 456
+
+        assert event.old_role.guild_id == 123
+        assert event.old_role.id == 456
diff --git a/tests/hikari/events/test_voice_events.py b/tests/hikari/events/test_voice_events.py
index 94b98840..dcef8c27 100644
--- a/tests/hikari/events/test_voice_events.py
+++ b/tests/hikari/events/test_voice_events.py
@@ -29,12 +29,18 @@
 class TestVoiceStateUpdateEvent:
     @pytest.fixture()
     def event(self):
-        return voice_events.VoiceStateUpdateEvent(app=None, shard=object(), state=mock.Mock(voices.VoiceState))
+        return voice_events.VoiceStateUpdateEvent(
+            app=None, shard=object(), state=mock.Mock(voices.VoiceState), old_state=mock.Mock(voices.VoiceState)
+        )
 
     def test_guild_id_property(self, event):
         event.state.guild_id = 123
         assert event.guild_id == 123
 
+    def test_old_voice_state(self, event):
+        event.old_state.guild_id = 123
+        assert event.old_state.guild_id == 123
+
 
 class TestVoiceServerUpdateEvent:
     @pytest.fixture()
diff --git a/tests/hikari/impl/test_buckets.py b/tests/hikari/impl/test_buckets.py
index 9792c10a..94cbbfe1 100644
--- a/tests/hikari/impl/test_buckets.py
+++ b/tests/hikari/impl/test_buckets.py
@@ -19,7 +19,6 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 import asyncio
-import datetime
 import time
 
 import mock
@@ -285,7 +284,7 @@ async def test_update_rate_limits_if_wrong_bucket_hash_reroutes_route(self):
             route = mock.Mock()
             route.create_real_bucket_hash = mock.Mock(wraps=lambda intial_hash: intial_hash + ";bobs")
             mgr.routes_to_hashes[route.route] = "123"
-            mgr.update_rate_limits(route, "blep", 22, 23, datetime.datetime.now(), datetime.datetime.now())
+            mgr.update_rate_limits(route, "blep", 22, 23, 3.56)
             assert mgr.routes_to_hashes[route.route] == "blep"
             assert isinstance(mgr.real_hashes_to_buckets["blep;bobs"], buckets.RESTBucket)
 
@@ -297,7 +296,7 @@ async def test_update_rate_limits_if_right_bucket_hash_does_nothing_to_hash(self
             mgr.routes_to_hashes[route.route] = "123"
             bucket = mock.Mock(reset_at=time.perf_counter() + 999999999999999999999999999)
             mgr.real_hashes_to_buckets["123;bobs"] = bucket
-            mgr.update_rate_limits(route, "123", 22, 23, datetime.datetime.now(), datetime.datetime.now())
+            mgr.update_rate_limits(route, "123", 22, 23, 7.65)
             assert mgr.routes_to_hashes[route.route] == "123"
             assert mgr.real_hashes_to_buckets["123;bobs"] is bucket
 
@@ -309,13 +308,10 @@ async def test_update_rate_limits_updates_params(self):
             mgr.routes_to_hashes[route.route] = "123"
             bucket = mock.Mock(reset_at=time.perf_counter() + 999999999999999999999999999)
             mgr.real_hashes_to_buckets["123;bobs"] = bucket
-            date = datetime.datetime.now().replace(year=2004)
-            reset_at = datetime.datetime.now()
 
             with mock.patch.object(hikari_date, "monotonic", return_value=27):
-                expect_reset_at_monotonic = 27 + (reset_at - date).total_seconds()
-                mgr.update_rate_limits(route, "123", 22, 23, date, reset_at)
-                bucket.update_rate_limit.assert_called_once_with(22, 23, expect_reset_at_monotonic)
+                mgr.update_rate_limits(route, "123", 22, 23, 5.32)
+                bucket.update_rate_limit.assert_called_once_with(22, 23, 27 + 5.32)
 
     @pytest.mark.parametrize(("gc_task", "is_started"), [(None, False), (mock.Mock(spec_set=asyncio.Task), True)])
     def test_is_started(self, gc_task, is_started):
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index 868285f4..a93cf6d4 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -245,11 +245,11 @@ def test_deserialize_application_with_unset_fields(self, entity_factory_impl, mo
                 "icon": "3123123",
                 "description": "I am an application",
                 "summary": "not a blank string",
+                "owner": owner_payload,
             }
         )
         assert application.is_bot_public is None
         assert application.is_bot_code_grant_required is None
-        assert application.owner is None
         assert application.rpc_origins is None
         assert application.verify_key is None
         assert application.team is None
@@ -267,6 +267,7 @@ def test_deserialize_application_with_null_fields(self, entity_factory_impl, moc
                 "description": "I am an application",
                 "summary": "not a blank string",
                 "team": None,
+                "owner": owner_payload,
             }
         )
         assert application.icon_hash is None
@@ -2522,7 +2523,7 @@ def test_deserialize_partial_message(
         assert partial_message.application.cover_image_hash == "58982a23790c4f22787b05d3be38a026"
         assert partial_message.application.primary_sku_id == 499494949494994
         assert isinstance(partial_message.application, message_models.MessageApplication)
-        # MessageCrosspost
+        # MessageReference
         assert partial_message.message_reference.app is mock_app
         assert partial_message.message_reference.id == 306588351130107906
         assert partial_message.message_reference.channel_id == 278325129692446722
@@ -2640,7 +2641,7 @@ def test_deserialize_full_message(
         assert message.application.primary_sku_id == 499494949494994
         assert isinstance(message.application, message_models.MessageApplication)
 
-        # MessageCrosspost
+        # MessageReference
         assert message.message_reference.app is mock_app
         assert message.message_reference.id == 306588351130107906
         assert message.message_reference.channel_id == 278325129692446722
diff --git a/tests/hikari/impl/test_stateful_cache.py b/tests/hikari/impl/test_stateful_cache.py
index c234a7d5..40140759 100644
--- a/tests/hikari/impl/test_stateful_cache.py
+++ b/tests/hikari/impl/test_stateful_cache.py
@@ -26,6 +26,7 @@
 from hikari import emojis
 from hikari import guilds
 from hikari import invites
+from hikari import messages
 from hikari import snowflakes
 from hikari import traits
 from hikari import users
@@ -44,7 +45,7 @@ def app_impl(self):
     @pytest.fixture()
     def cache_impl(self, app_impl) -> stateful_cache.StatefulCacheImpl:
         return hikari_test_helpers.mock_class_namespace(stateful_cache.StatefulCacheImpl, slots_=False)(
-            app=app_impl, intents=None
+            app=app_impl, intents=None, settings=None
         )
 
     def test__build_emoji(self, cache_impl):
@@ -2473,3 +2474,51 @@ def test_update_voice_state(self, cache_impl):
                 mock.call(snowflakes.Snowflake(43123123), snowflakes.Snowflake(542134)),
             ]
         )
+
+    def test_clear_messages(self, cache_impl):
+        messages_ = {1: mock.Mock(messages.Message, id=123), 2: mock.Mock(messages.Message, id=456)}
+        cache_impl._message_entries = collections.LimitedCapacityCacheMap(messages_, limit=2)
+
+        view = cache_impl.clear_messages()
+        assert view.get(1).id == 123
+        assert view.get(2).id == 456
+
+    def test_delete_message(self, cache_impl):
+        message = mock.Mock(messages.Message, id=123)
+        cache_impl._message_entries = collections.LimitedCapacityCacheMap({1: message}, limit=1)
+
+        msg = cache_impl.delete_message(1)
+        assert msg.id == 123
+        assert len(cache_impl._message_entries) == 0
+
+    def test_get_message(self, cache_impl):
+        message = mock.Mock(messages.Message, id=123)
+        cache_impl._message_entries = collections.LimitedCapacityCacheMap({1: message}, limit=1)
+
+        assert cache_impl._message_entries.get(1).id == 123
+
+    def test_get_messages_view(self, cache_impl):
+        messages_ = {1: mock.Mock(messages.Message, id=1234567890)}
+
+        cache_impl._message_entries = collections.FreezableDict(messages_)
+        view = cache_impl.get_messages_view()
+
+        assert view._data == messages_
+
+    def test_set_message(self, cache_impl):
+        message = mock.Mock(messages.Message, id=123)
+        cache_impl._message_entries = collections.LimitedCapacityCacheMap(limit=1)
+        cache_impl._message_entries[1] = message
+
+        assert cache_impl._message_entries.get(1).id == 123
+        cache_impl._message_entries[2] = message
+        assert len(cache_impl._message_entries) == 1
+
+    def test_update_message(self, cache_impl):
+        message = mock.Mock(messages.Message, id=1, content="New test.")
+        old_message = mock.Mock(messages.Message, id=1, content="Old test.")
+        cache_impl._message_entries = collections.LimitedCapacityCacheMap({1: old_message}, limit=1)
+
+        assert cache_impl._message_entries.get(1).content == "Old test."
+        cache_impl.update_message(message)
+        assert cache_impl._message_entries.get(1).content == "New test."
diff --git a/tests/hikari/impl/test_stateful_event_manager.py b/tests/hikari/impl/test_stateful_event_manager.py
new file mode 100644
index 00000000..efe8f3b8
--- /dev/null
+++ b/tests/hikari/impl/test_stateful_event_manager.py
@@ -0,0 +1,635 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Nekokatt
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import mock
+import pytest
+
+from hikari import channels
+from hikari import intents
+from hikari.events import shard_events
+from hikari.impl import stateful_event_manager
+from tests.hikari import hikari_test_helpers
+
+
+class TestStatefulEventManagerImpl:
+    @pytest.fixture()
+    def app(self):
+        return mock.Mock()
+
+    @pytest.fixture()
+    def shard(self):
+        return mock.Mock()
+
+    @pytest.fixture()
+    def event_manager(self, app):
+        obj = hikari_test_helpers.mock_class_namespace(stateful_event_manager.StatefulEventManagerImpl, slots_=False)(
+            app, mock.Mock(), intents.Intents.ALL
+        )
+
+        obj.dispatch = mock.AsyncMock()
+        return obj
+
+    @pytest.mark.asyncio
+    async def test_on_connected(self, event_manager, shard, app):
+        payload = {}
+        event = shard_events.ShardConnectedEvent(app=app, shard=shard)
+
+        await event_manager.on_connected(shard, payload)
+
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_disconnected(self, event_manager, shard, app):
+        payload = {}
+        event = shard_events.ShardDisconnectedEvent(app=app, shard=shard)
+
+        await event_manager.on_disconnected(shard, payload)
+
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_ready(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock(my_user=mock.Mock())
+
+        event_manager._app.event_factory.deserialize_ready_event.return_value = event
+
+        await event_manager.on_ready(shard, payload)
+
+        event_manager._cache.update_me.assert_called_once_with(event.my_user)
+        event_manager._app.event_factory.deserialize_ready_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_resumed(self, event_manager, shard, app):
+        payload = {}
+        event = shard_events.ShardResumedEvent(app=app, shard=shard)
+
+        await event_manager.on_resumed(shard, payload)
+
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_channel_create(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock(channel=mock.Mock(channels.GuildChannel))
+
+        event_manager._app.event_factory.deserialize_channel_create_event.return_value = event
+
+        await event_manager.on_channel_create(shard, payload)
+
+        event_manager._cache.set_guild_channel.assert_called_once_with(event.channel)
+        event_manager._app.event_factory.deserialize_channel_create_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_channel_update(self, event_manager, shard, app):
+        payload = {"id": 123}
+        old_channel = object()
+        event = mock.Mock(channel=mock.Mock(channels.GuildChannel))
+
+        event_manager._app.event_factory.deserialize_channel_update_event.return_value = event
+        event_manager._cache.get_guild_channel.return_value = old_channel
+
+        await event_manager.on_channel_update(shard, payload)
+
+        event_manager._cache.get_guild_channel.assert_called_once_with(123)
+        event_manager._cache.update_guild_channel.assert_called_once_with(event.channel)
+        event_manager._app.event_factory.deserialize_channel_update_event.assert_called_once_with(
+            shard, payload, old_channel=old_channel
+        )
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_channel_delete(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock(channel=mock.Mock(id=123))
+
+        event_manager._app.event_factory.deserialize_channel_delete_event.return_value = event
+
+        await event_manager.on_channel_delete(shard, payload)
+
+        event_manager._cache.delete_guild_channel.assert_called_once_with(123)
+        event_manager._app.event_factory.deserialize_channel_delete_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_channel_pins_update(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock()
+
+        event_manager._app.event_factory.deserialize_channel_pins_update_event.return_value = event
+
+        await event_manager.on_channel_pins_update(shard, payload)
+
+        event_manager._app.event_factory.deserialize_channel_pins_update_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_guild_create(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock(
+            guild=mock.Mock(id=123),
+            channels={"TestChannel": 456},
+            emojis={"TestEmoji": 789},
+            roles={"TestRole": 1234},
+            members={"TestMember": 5678},
+            presences={"TestPresence": 9012},
+            voice_states={"TestState": 345},
+        )
+
+        event_manager._app.event_factory.deserialize_guild_create_event.return_value = event
+        shard.request_guild_members = mock.AsyncMock()
+
+        await event_manager.on_guild_create(shard, payload)
+
+        event_manager._cache.update_guild.assert_called_once_with(event.guild)
+
+        event_manager._cache.clear_guild_channels_for_guild.assert_called_once_with(123)
+        event_manager._cache.set_guild_channel.assert_called_once_with(456)
+
+        event_manager._cache.clear_emojis_for_guild.assert_called_once_with(123)
+        event_manager._cache.set_emoji.assert_called_once_with(789)
+
+        event_manager._cache.clear_roles_for_guild.assert_called_once_with(123)
+        event_manager._cache.set_role.assert_called_once_with(1234)
+
+        event_manager._cache.clear_members_for_guild.assert_called_once_with(123)
+        event_manager._cache.set_member.assert_called_once_with(5678)
+
+        event_manager._cache.clear_presences_for_guild.assert_called_once_with(123)
+        event_manager._cache.set_presence.assert_called_once_with(9012)
+
+        event_manager._cache.clear_voice_states_for_guild.assert_called_once_with(123)
+        event_manager._cache.set_voice_state.assert_called_once_with(345)
+
+        event_manager._app.event_factory.deserialize_guild_create_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_guild_update(self, event_manager, shard, app):
+        payload = {"id": 123}
+        old_guild = object()
+        event = mock.Mock(roles={}, emojis={}, guild=mock.Mock(id=123))
+
+        event_manager._app.event_factory.deserialize_guild_update_event.return_value = event
+        event_manager._cache.get_guild.return_value = old_guild
+
+        await event_manager.on_guild_update(shard, payload)
+
+        event_manager._cache.get_guild.assert_called_once_with(123)
+        event_manager._cache.update_guild.assert_called_once_with(event.guild)
+        event_manager._cache.clear_roles_for_guild.assert_called_once_with(123)
+        event_manager._cache.clear_emojis_for_guild.assert_called_once_with(123)
+        event_manager._app.event_factory.deserialize_guild_update_event.assert_called_once_with(
+            shard, payload, old_guild=old_guild
+        )
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_guild_delete(self, event_manager, shard, app):
+        payload = {"unavailable": False}
+        event = mock.Mock(guild_id=123)
+
+        event_manager._app.event_factory.deserialize_guild_leave_event.return_value = event
+
+        await event_manager.on_guild_delete(shard, payload)
+
+        event_manager._cache.delete_guild.assert_called_once_with(123)
+        event_manager._cache.clear_voice_states_for_guild.assert_called_once_with(123)
+        event_manager._cache.clear_invites_for_guild.assert_called_once_with(123)
+        event_manager._cache.clear_members_for_guild.assert_called_once_with(123)
+        event_manager._cache.clear_presences_for_guild.assert_called_once_with(123)
+        event_manager._cache.clear_guild_channels_for_guild.assert_called_once_with(123)
+        event_manager._cache.clear_emojis_for_guild.assert_called_once_with(123)
+        event_manager._cache.clear_roles_for_guild.assert_called_once_with(123)
+        event_manager._app.event_factory.deserialize_guild_leave_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_guild_ban_add(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock()
+
+        event_manager._app.event_factory.deserialize_guild_ban_add_event.return_value = event
+
+        await event_manager.on_guild_ban_add(shard, payload)
+
+        event_manager._app.event_factory.deserialize_guild_ban_add_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_guild_ban_remove(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock()
+
+        event_manager._app.event_factory.deserialize_guild_ban_remove_event.return_value = event
+
+        await event_manager.on_guild_ban_remove(shard, payload)
+
+        event_manager._app.event_factory.deserialize_guild_ban_remove_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_guild_emojis_update(self, event_manager, shard, app):
+        payload = {"guild_id": 123}
+        old_emojis = {"Test": 123}
+        event = mock.Mock(emojis={}, guild_id=123)
+
+        event_manager._app.event_factory.deserialize_guild_emojis_update_event.return_value = event
+        event_manager._cache.get_emojis_view_for_guild.return_value = old_emojis
+
+        await event_manager.on_guild_emojis_update(shard, payload)
+
+        event_manager._cache.get_emojis_view_for_guild.assert_called_once_with(123)
+        event_manager._cache.clear_emojis_for_guild.assert_called_once_with(123)
+        event_manager._app.event_factory.deserialize_guild_emojis_update_event.assert_called_once_with(
+            shard, payload, old_emojis=[123]
+        )
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_integration_create(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock()
+
+        event_manager._app.event_factory.deserialize_integration_create_event.return_value = event
+
+        await event_manager.on_integration_create(shard, payload)
+
+        event_manager._app.event_factory.deserialize_integration_create_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_integration_delete(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock()
+
+        event_manager._app.event_factory.deserialize_integration_delete_event.return_value = event
+
+        await event_manager.on_integration_delete(shard, payload)
+
+        event_manager._app.event_factory.deserialize_integration_delete_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_integration_update(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock()
+
+        event_manager._app.event_factory.deserialize_integration_update_event.return_value = event
+
+        await event_manager.on_integration_update(shard, payload)
+
+        event_manager._app.event_factory.deserialize_integration_update_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_guild_member_add(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock(user=object(), member=object())
+
+        event_manager._app.event_factory.deserialize_guild_member_add_event.return_value = event
+
+        await event_manager.on_guild_member_add(shard, payload)
+
+        event_manager._cache.update_user.assert_called_once_with(event.user)
+        event_manager._cache.update_member.assert_called_once_with(event.member)
+        event_manager._app.event_factory.deserialize_guild_member_add_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_guild_member_remove(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock(user=mock.Mock(id=123), guild_id=456)
+
+        event_manager._app.event_factory.deserialize_guild_member_remove_event.return_value = event
+
+        await event_manager.on_guild_member_remove(shard, payload)
+
+        event_manager._cache.update_user.assert_called_once_with(event.user)
+        event_manager._cache.delete_member.assert_called_once_with(456, 123)
+        event_manager._app.event_factory.deserialize_guild_member_remove_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_guild_member_update(self, event_manager, shard, app):
+        payload = {"user": {"id": 123}, "guild_id": 456}
+        old_member = object()
+        event = mock.Mock(member=mock.Mock())
+
+        event_manager._app.event_factory.deserialize_guild_member_update_event.return_value = event
+        event_manager._cache.get_member.return_value = old_member
+
+        await event_manager.on_guild_member_update(shard, payload)
+
+        event_manager._cache.get_member.assert_called_once_with(456, 123)
+        event_manager._cache.update_member.assert_called_once_with(event.member)
+        event_manager._app.event_factory.deserialize_guild_member_update_event.assert_called_once_with(
+            shard, payload, old_member=old_member
+        )
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_guild_members_chunk(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock(members={"TestMember": 123}, presences={"TestPresences": 456})
+
+        event_manager._app.event_factory.deserialize_guild_member_chunk_event.return_value = event
+        event_manager._app.chunker.consume_chunk_event = mock.AsyncMock()
+
+        await event_manager.on_guild_members_chunk(shard, payload)
+
+        event_manager._cache.set_member.assert_called_once_with(123)
+        event_manager._cache.set_presence.assert_called_once_with(456)
+        event_manager._app.event_factory.deserialize_guild_member_chunk_event.assert_called_once_with(shard, payload)
+        event_manager._app.chunker.consume_chunk_event.assert_awaited_once_with(event)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_guild_role_create(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock(role=object())
+
+        event_manager._app.event_factory.deserialize_guild_role_create_event.return_value = event
+
+        await event_manager.on_guild_role_create(shard, payload)
+
+        event_manager._cache.set_role.assert_called_once_with(event.role)
+        event_manager._app.event_factory.deserialize_guild_role_create_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_guild_role_update(self, event_manager, shard, app):
+        payload = {"role": {"id": 123}}
+        old_role = object()
+        event = mock.Mock(role=mock.Mock())
+
+        event_manager._app.event_factory.deserialize_guild_role_update_event.return_value = event
+        event_manager._cache.get_role.return_value = old_role
+
+        await event_manager.on_guild_role_update(shard, payload)
+
+        event_manager._cache.get_role.assert_called_once_with(123)
+        event_manager._cache.update_role.assert_called_once_with(event.role)
+        event_manager._app.event_factory.deserialize_guild_role_update_event.assert_called_once_with(
+            shard, payload, old_role=old_role
+        )
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_guild_role_delete(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock(role_id=123)
+
+        event_manager._app.event_factory.deserialize_guild_role_delete_event.return_value = event
+
+        await event_manager.on_guild_role_delete(shard, payload)
+
+        event_manager._cache.delete_role.assert_called_once_with(123)
+        event_manager._app.event_factory.deserialize_guild_role_delete_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_invite_create(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock(invite="qwerty")
+
+        event_manager._app.event_factory.deserialize_invite_create_event.return_value = event
+
+        await event_manager.on_invite_create(shard, payload)
+
+        event_manager._cache.set_invite.assert_called_once_with("qwerty")
+        event_manager._app.event_factory.deserialize_invite_create_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_invite_delete(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock(code="qwerty")
+
+        event_manager._app.event_factory.deserialize_invite_delete_event.return_value = event
+
+        await event_manager.on_invite_delete(shard, payload)
+
+        event_manager._cache.delete_invite.assert_called_once_with("qwerty")
+        event_manager._app.event_factory.deserialize_invite_delete_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_message_create(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock(message=object())
+
+        event_manager._app.event_factory.deserialize_message_create_event.return_value = event
+
+        await event_manager.on_message_create(shard, payload)
+
+        event_manager._cache.set_message.assert_called_once_with(event.message)
+        event_manager._app.event_factory.deserialize_message_create_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_message_update(self, event_manager, shard, app):
+        payload = {"id": 123}
+        old_message = object()
+        event = mock.Mock(message=mock.Mock())
+
+        event_manager._app.event_factory.deserialize_message_update_event.return_value = event
+        event_manager._cache.get_message.return_value = old_message
+
+        await event_manager.on_message_update(shard, payload)
+
+        event_manager._cache.get_message.assert_called_once_with(123)
+        event_manager._cache.update_message.assert_called_once_with(event.message)
+        event_manager._app.event_factory.deserialize_message_update_event.assert_called_once_with(
+            shard, payload, old_message=old_message
+        )
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_message_delete(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock(message_id=123)
+
+        event_manager._app.event_factory.deserialize_message_delete_event.return_value = event
+
+        await event_manager.on_message_delete(shard, payload)
+
+        event_manager._cache.delete_message.assert_called_once_with(123)
+        event_manager._app.event_factory.deserialize_message_delete_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_message_delete_bulk(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock(message_ids=[123, 456, 789])
+
+        event_manager._app.event_factory.deserialize_message_delete_bulk_event.return_value = event
+
+        await event_manager.on_message_delete_bulk(shard, payload)
+
+        assert len(event_manager._cache.delete_message.mock_calls) == 3
+        event_manager._app.event_factory.deserialize_message_delete_bulk_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_message_reaction_add(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock()
+
+        event_manager._app.event_factory.deserialize_message_reaction_add_event.return_value = event
+
+        await event_manager.on_message_reaction_add(shard, payload)
+
+        event_manager._app.event_factory.deserialize_message_reaction_add_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_message_reaction_remove(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock()
+
+        event_manager._app.event_factory.deserialize_message_reaction_remove_event.return_value = event
+
+        await event_manager.on_message_reaction_remove(shard, payload)
+
+        event_manager._app.event_factory.deserialize_message_reaction_remove_event.assert_called_once_with(
+            shard, payload
+        )
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_message_reaction_remove_all(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock()
+
+        event_manager._app.event_factory.deserialize_message_reaction_remove_all_event.return_value = event
+
+        await event_manager.on_message_reaction_remove_all(shard, payload)
+
+        event_manager._app.event_factory.deserialize_message_reaction_remove_all_event.assert_called_once_with(
+            shard, payload
+        )
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_message_reaction_remove_emoji(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock()
+
+        event_manager._app.event_factory.deserialize_message_reaction_remove_emoji_event.return_value = event
+
+        await event_manager.on_message_reaction_remove_emoji(shard, payload)
+
+        event_manager._app.event_factory.deserialize_message_reaction_remove_emoji_event.assert_called_once_with(
+            shard, payload
+        )
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_presence_update(self, event_manager, shard, app):
+        payload = {"user": {"id": 123}, "guild_id": 456}
+        old_presence = object()
+        event = mock.Mock(presence=mock.Mock())
+
+        event_manager._app.event_factory.deserialize_presence_update_event.return_value = event
+        event_manager._cache.get_presence.return_value = old_presence
+
+        await event_manager.on_presence_update(shard, payload)
+
+        event_manager._cache.get_presence.assert_called_once_with(456, 123)
+        event_manager._cache.update_presence.assert_called_once_with(event.presence)
+        event_manager._app.event_factory.deserialize_presence_update_event.assert_called_once_with(
+            shard, payload, old_presence=old_presence
+        )
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_typing_start(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock()
+
+        event_manager._app.event_factory.deserialize_typing_start_event.return_value = event
+
+        await event_manager.on_typing_start(shard, payload)
+
+        event_manager._app.event_factory.deserialize_typing_start_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_user_update(self, event_manager, shard, app):
+        payload = {}
+        old_user = object()
+        event = mock.Mock(user=mock.Mock())
+
+        event_manager._app.event_factory.deserialize_own_user_update_event.return_value = event
+        event_manager._cache.get_me.return_value = old_user
+
+        await event_manager.on_user_update(shard, payload)
+
+        event_manager._cache.update_me.assert_called_once_with(event.user)
+        event_manager._app.event_factory.deserialize_own_user_update_event.assert_called_once_with(
+            shard, payload, old_user=old_user
+        )
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_voice_state_update(self, event_manager, shard, app):
+        payload = {"user_id": 123, "guild_id": 456}
+        old_state = object()
+        event = mock.Mock(state=mock.Mock())
+
+        event_manager._app.event_factory.deserialize_voice_state_update_event.return_value = event
+        event_manager._cache.get_voice_state.return_value = old_state
+
+        await event_manager.on_voice_state_update(shard, payload)
+
+        event_manager._cache.get_voice_state.assert_called_once_with(456, 123)
+        event_manager._cache.update_voice_state.assert_called_once_with(event.state)
+        event_manager._app.event_factory.deserialize_voice_state_update_event.assert_called_once_with(
+            shard, payload, old_state=old_state
+        )
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_voice_server_update(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock()
+
+        event_manager._app.event_factory.deserialize_voice_server_update_event.return_value = event
+
+        await event_manager.on_voice_server_update(shard, payload)
+
+        event_manager._app.event_factory.deserialize_voice_server_update_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
+
+    @pytest.mark.asyncio
+    async def test_on_webhooks_update(self, event_manager, shard, app):
+        payload = {}
+        event = mock.Mock()
+
+        event_manager._app.event_factory.deserialize_webhook_update_event.return_value = event
+
+        await event_manager.on_webhooks_update(shard, payload)
+
+        event_manager._app.event_factory.deserialize_webhook_update_event.assert_called_once_with(shard, payload)
+        event_manager.dispatch.assert_awaited_once_with(event)
diff --git a/tests/hikari/impl/test_stateless_cache.py b/tests/hikari/impl/test_stateless_cache.py
index 79a0bc23..f7620f76 100644
--- a/tests/hikari/impl/test_stateless_cache.py
+++ b/tests/hikari/impl/test_stateless_cache.py
@@ -339,3 +339,25 @@ def test_set_voice_state(self, component):
     def test_update_voice_state(self, component):
         with pytest.raises(NotImplementedError):
             assert component.update_voice_state(object())
+
+    def test_clear_messages(self, component):
+        with pytest.raises(NotImplementedError):
+            assert component.clear_messages()
+
+    def test_delete_message(self, component):
+        with pytest.raises(NotImplementedError):
+            assert component.delete_message(123)
+
+    def test_get_message(self, component):
+        assert component.get_message(123) is None
+
+    def test_get_messages_view(self, component):
+        assert component.get_messages_view() == {}
+
+    def test_set_message(self, component):
+        with pytest.raises(NotImplementedError):
+            assert component.set_message(object())
+
+    def test_update_message(self, component):
+        with pytest.raises(NotImplementedError):
+            assert component.update_message(object())
diff --git a/tests/hikari/impl/test_stateless_event_manager.py b/tests/hikari/impl/test_stateless_event_manager.py
index e5d9e3b8..dc6d8e49 100644
--- a/tests/hikari/impl/test_stateless_event_manager.py
+++ b/tests/hikari/impl/test_stateless_event_manager.py
@@ -95,7 +95,9 @@ async def test_on_channel_update(self, event_manager, shard, payload):
         await event_manager.on_channel_update(shard, payload)
 
         event_manager.dispatch.assert_awaited_once_with(event)
-        event_manager._app.event_factory.deserialize_channel_update_event.assert_called_once_with(shard, payload)
+        event_manager._app.event_factory.deserialize_channel_update_event.assert_called_once_with(
+            shard, payload, old_channel=None
+        )
 
     async def test_on_channel_delete(self, event_manager, shard, payload):
         event = object()
@@ -131,7 +133,9 @@ async def test_on_guild_update(self, event_manager, shard, payload):
         await event_manager.on_guild_update(shard, payload)
 
         event_manager.dispatch.assert_awaited_once_with(event)
-        event_manager._app.event_factory.deserialize_guild_update_event.assert_called_once_with(shard, payload)
+        event_manager._app.event_factory.deserialize_guild_update_event.assert_called_once_with(
+            shard, payload, old_guild=None
+        )
 
     async def test_on_guild_delete(self, event_manager, shard):
         payload = {"unavailable": False}
@@ -178,7 +182,9 @@ async def test_on_guild_emojis_update(self, event_manager, shard, payload):
         await event_manager.on_guild_emojis_update(shard, payload)
 
         event_manager.dispatch.assert_awaited_once_with(event)
-        event_manager._app.event_factory.deserialize_guild_emojis_update_event.assert_called_once_with(shard, payload)
+        event_manager._app.event_factory.deserialize_guild_emojis_update_event.assert_called_once_with(
+            shard, payload, old_emojis=None
+        )
 
     async def test_on_integration_create(self, event_manager, shard, payload):
         event = object()
@@ -232,7 +238,9 @@ async def test_on_guild_member_update(self, event_manager, shard, payload):
         await event_manager.on_guild_member_update(shard, payload)
 
         event_manager.dispatch.assert_awaited_once_with(event)
-        event_manager._app.event_factory.deserialize_guild_member_update_event.assert_called_once_with(shard, payload)
+        event_manager._app.event_factory.deserialize_guild_member_update_event.assert_called_once_with(
+            shard, payload, old_member=None
+        )
 
     async def test_on_guild_members_chunk(self, event_manager, shard, payload):
         event = object()
@@ -259,7 +267,9 @@ async def test_on_guild_role_update(self, event_manager, shard, payload):
         await event_manager.on_guild_role_update(shard, payload)
 
         event_manager.dispatch.assert_awaited_once_with(event)
-        event_manager._app.event_factory.deserialize_guild_role_update_event.assert_called_once_with(shard, payload)
+        event_manager._app.event_factory.deserialize_guild_role_update_event.assert_called_once_with(
+            shard, payload, old_role=None
+        )
 
     async def test_on_guild_role_delete(self, event_manager, shard, payload):
         event = object()
@@ -304,7 +314,9 @@ async def test_on_message_update(self, event_manager, shard, payload):
         await event_manager.on_message_update(shard, payload)
 
         event_manager.dispatch.assert_awaited_once_with(event)
-        event_manager._app.event_factory.deserialize_message_update_event.assert_called_once_with(shard, payload)
+        event_manager._app.event_factory.deserialize_message_update_event.assert_called_once_with(
+            shard, payload, old_message=None
+        )
 
     async def test_on_message_delete(self, event_manager, shard, payload):
         event = object()
@@ -373,7 +385,9 @@ async def test_on_presence_update(self, event_manager, shard, payload):
         await event_manager.on_presence_update(shard, payload)
 
         event_manager.dispatch.assert_awaited_once_with(event)
-        event_manager._app.event_factory.deserialize_presence_update_event.assert_called_once_with(shard, payload)
+        event_manager._app.event_factory.deserialize_presence_update_event.assert_called_once_with(
+            shard, payload, old_presence=None
+        )
 
     async def test_on_typing_start(self, event_manager, shard, payload):
         event = object()
@@ -391,7 +405,9 @@ async def test_on_user_update(self, event_manager, shard, payload):
         await event_manager.on_user_update(shard, payload)
 
         event_manager.dispatch.assert_awaited_once_with(event)
-        event_manager._app.event_factory.deserialize_own_user_update_event.assert_called_once_with(shard, payload)
+        event_manager._app.event_factory.deserialize_own_user_update_event.assert_called_once_with(
+            shard, payload, old_user=None
+        )
 
     async def test_on_voice_state_update(self, event_manager, shard, payload):
         event = object()
@@ -400,7 +416,9 @@ async def test_on_voice_state_update(self, event_manager, shard, payload):
         await event_manager.on_voice_state_update(shard, payload)
 
         event_manager.dispatch.assert_awaited_once_with(event)
-        event_manager._app.event_factory.deserialize_voice_state_update_event.assert_called_once_with(shard, payload)
+        event_manager._app.event_factory.deserialize_voice_state_update_event.assert_called_once_with(
+            shard, payload, old_state=None
+        )
 
     async def test_on_voice_server_update(self, event_manager, shard, payload):
         event = object()
diff --git a/tests/hikari/internal/test_time.py b/tests/hikari/internal/test_time.py
index b64bcd05..fdce16be 100644
--- a/tests/hikari/internal/test_time.py
+++ b/tests/hikari/internal/test_time.py
@@ -92,12 +92,6 @@ def test_parse_iso_8601_date_with_no_fraction():
     assert date.microsecond == 0
 
 
-def test_parse_http_date():
-    rfc_timestamp = "Mon, 03 Jun 2019 17:54:26 GMT"
-    expected_timestamp = datetime.datetime(2019, 6, 3, 17, 54, 26, tzinfo=datetime.timezone.utc)
-    assert time.rfc7231_datetime_string_to_datetime(rfc_timestamp) == expected_timestamp
-
-
 def test_parse_discord_epoch_to_datetime():
     discord_timestamp = 37921278956
     expected_timestamp = datetime.datetime(2016, 3, 14, 21, 41, 18, 956000, tzinfo=datetime.timezone.utc)