Repository: hikari-py/hikari
Pull request #641 (2021-06-03 10:04:56)

Improve loop acquisition in BotApp.run
### Summary
Improve loop acquisition in BotApp.run

### 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
Fixes: #626


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8e8ce9c0..17a4942f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -102,9 +102,17 @@ jobs:
         with:
           python-version: 3.8
 
-      - name: Linting
+      - name: Check stubs
         run: |
           pip install nox
+          nox -s generate-stubs
+          if [ "$(git status --short)" ]; then
+            echo "Stubs were not updated accordingly to the changes. Please run 'nox -s generate-stubs' and commit the changes to fix this."
+            exit 1
+          fi
+
+      - name: Linting
+        run: |
           nox -s safety mypy flake8
 
   twemoji:
diff --git a/.gitignore b/.gitignore
index d148405b..c46fd678 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,8 +60,5 @@ credentials.env
 # Profiling in pytest
 prof/
 
-# Generated stubs for MyPy --strict hackfixes
-__init__.pyi
-
 # Logs
 *.log
diff --git a/README.md b/README.md
index 1a59a4cc..a7a99a16 100644
--- a/README.md
+++ b/README.md
@@ -176,9 +176,9 @@ and `ciso8601`, which will provide you with a small performance boost.
 
 ### `uvloop`
 
-**If you use Linux**, you will get additional performance benefits from using
-a library called `uvloop`. This replaces the default `asyncio` event loop with
-one that uses `libuv` internally. You can run `pip install uvloop` and then
+**If you use a UNIX-like system**, you will get additional performance benefits
+from using a library called `uvloop`. This replaces the default `asyncio` event
+loop with one that uses `libuv` internally. You can run `pip install uvloop` and then
 amend your script to be something similar to the following example to utilise it
 in your application:
 
@@ -190,7 +190,7 @@ if os.name != "nt":
     import uvloop
     uvloop.install()
 
-bot = hikari.Bot(...)
+bot = hikari.BotApp(...)
 ...
 ```
 
diff --git a/dev-requirements.txt b/dev-requirements.txt
index cf67eddb..5cddde4d 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -28,7 +28,7 @@ sphobjinv==2.1
 # TYPE CHECKING #
 #################
 
-mypy==0.812
+mypy==0.902
 
 #######################
 # DEPENDENCY CHECKING #
@@ -40,5 +40,5 @@ safety~=1.10.3
 # FORMATING #
 #############
 
-black==21.5b2
+black==21.6b0
 isort==5.8.0
diff --git a/hikari/__init__.pyi b/hikari/__init__.pyi
new file mode 100644
index 00000000..f7e7f5ef
--- /dev/null
+++ b/hikari/__init__.pyi
@@ -0,0 +1,34 @@
+# This file was generated by `nox -s generate-stubs`. Do not manually edit this file!
+
+from hikari.audit_logs import *
+from hikari.channels import *
+from hikari.colors import *
+from hikari.colours import *
+from hikari.config import *
+from hikari.embeds import *
+from hikari.emojis import *
+from hikari.errors import *
+from hikari.events import *
+from hikari.guilds import *
+from hikari.intents import *
+from hikari.invites import *
+from hikari.iterators import *
+from hikari.messages import *
+from hikari.permissions import *
+from hikari.presences import *
+from hikari.sessions import *
+from hikari.templates import *
+from hikari.traits import *
+from hikari.users import *
+from hikari.voices import *
+from hikari.webhooks import *
+from hikari._about import __author__ as __author__, __ci__ as __ci__, __copyright__ as __copyright__, __discord_invite__ as __discord_invite__, __docs__ as __docs__, __email__ as __email__, __issue_tracker__ as __issue_tracker__, __license__ as __license__, __url__ as __url__, __version__ as __version__
+from hikari.applications import Application as Application, ApplicationFlags as ApplicationFlags, AuthorizationApplication as AuthorizationApplication, AuthorizationInformation as AuthorizationInformation, ConnectionVisibility as ConnectionVisibility, OAuth2AuthorizationToken as OAuth2AuthorizationToken, OAuth2ImplicitToken as OAuth2ImplicitToken, OAuth2Scope as OAuth2Scope, OwnConnection as OwnConnection, OwnGuild as OwnGuild, PartialOAuth2Token as PartialOAuth2Token, Team as Team, TeamMember as TeamMember, TeamMembershipState as TeamMembershipState, TokenType as TokenType
+from hikari.files import Bytes as Bytes, File as File, LazyByteIteratorish as LazyByteIteratorish, Pathish as Pathish, Rawish as Rawish, Resourceish as Resourceish, URL as URL
+from hikari.impl.bot import BotApp as BotApp
+from hikari.impl.rest import ClientCredentialsStrategy as ClientCredentialsStrategy, RESTApp as RESTApp
+from hikari.snowflakes import SearchableSnowflakeish as SearchableSnowflakeish, SearchableSnowflakeishOr as SearchableSnowflakeishOr, Snowflake as Snowflake, Snowflakeish as Snowflakeish, SnowflakeishOr as SnowflakeishOr, SnowflakeishSequence as SnowflakeishSequence, Unique as Unique
+from hikari.undefined import UNDEFINED as UNDEFINED, UndefinedNoneOr as UndefinedNoneOr, UndefinedOr as UndefinedOr, UndefinedType as UndefinedType
+from typing import Any
+
+__all__: Any
diff --git a/hikari/api/cache.py b/hikari/api/cache.py
index 013fe6a8..d86cd989 100644
--- a/hikari/api/cache.py
+++ b/hikari/api/cache.py
@@ -78,6 +78,34 @@ class Cache(abc.ABC):
 
     __slots__: typing.Sequence[str] = ()
 
+    @abc.abstractmethod
+    def get_dm_channel_id(
+        self, user: snowflakes.SnowflakeishOr[users.PartialUser], /
+    ) -> typing.Optional[snowflakes.Snowflake]:
+        """Get the DM channel ID for a user.
+
+        Parameters
+        ----------
+        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
+            Objet or ID of the user to get the DM channel ID for.
+
+        Returns
+        -------
+        typing.Optional[hikari.snowflakes.Snowflake]
+            ID of the DM channel which was found cached for the supplied user or
+            `builtins.None`.
+        """
+
+    @abc.abstractmethod
+    def get_dm_channel_ids_view(self) -> CacheView[snowflakes.Snowflake, snowflakes.Snowflake]:
+        """Get a view of the cached DM channel IDs.
+
+        Returns
+        -------
+        CacheView[hikari.snowflakes.Snowflake, hikari.snowflakes.Snowflake]
+            Cache view of user IDs to DM channel IDs.
+        """
+
     @abc.abstractmethod
     def get_emoji(
         self, emoji: snowflakes.SnowflakeishOr[emojis.CustomEmoji], /
@@ -638,6 +666,52 @@ class MutableCache(Cache, abc.ABC):
     def clear(self) -> None:
         """Clear the full cache."""
 
+    @abc.abstractmethod
+    def clear_dm_channel_ids(self) -> CacheView[snowflakes.Snowflake, snowflakes.Snowflake]:
+        """Remove all the cached DM channel IDs.
+
+        Returns
+        -------
+        CacheView[hikari.snowflakes.Snowflake, hikari.snowflakes.Snowflake]
+            Cache view of user IDs to DM channel IDs which were cleared from
+            the cache.
+        """
+
+    @abc.abstractmethod
+    def delete_dm_channel_id(
+        self, user: snowflakes.SnowflakeishOr[users.PartialUser], /
+    ) -> typing.Optional[snowflakes.Snowflake]:
+        """Remove a DM channel ID from the cache.
+
+        Parameters
+        ----------
+        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
+            Object or ID of the user to remove the cached DM channel ID for.
+
+        Returns
+        -------
+        typing.Optional[hikari.snowflakes.Snowflake]
+            The DM channel ID which was removed from the cache if found, else
+            `builtins.None`.
+        """
+
+    @abc.abstractmethod
+    def set_dm_channel_id(
+        self,
+        user: snowflakes.SnowflakeishOr[users.PartialUser],
+        channel: snowflakes.SnowflakeishOr[channels.PartialChannel],
+        /,
+    ) -> None:
+        """Add a DM channel ID to the cache.
+
+        Parameters
+        ----------
+        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
+            Object or ID of the user to add a DM channel ID to the cache for.
+        channel : hikari.snowflakes.SnowflakeishOr[hikari.channels.PartialChannel]
+            Object or ID of the DM channel to add to the cache.
+        """
+
     @abc.abstractmethod
     def clear_emojis(self) -> CacheView[snowflakes.Snowflake, emojis.KnownCustomEmoji]:
         """Remove all the known custom emoji objects from the cache.
diff --git a/hikari/api/event_factory.py b/hikari/api/event_factory.py
index b2294b89..679207ff 100644
--- a/hikari/api/event_factory.py
+++ b/hikari/api/event_factory.py
@@ -829,7 +829,7 @@ def deserialize_message_reaction_remove_emoji_event(
     @abc.abstractmethod
     def deserialize_shard_payload_event(
         self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject, *, name: str
-    ) -> shard_events.ShardPayload:
+    ) -> shard_events.ShardPayloadEvent:
         """Parse a raw payload from Discord into a shard payload event object.
 
         Parameters
@@ -843,7 +843,7 @@ def deserialize_shard_payload_event(
 
         Returns
         -------
-        hikari.events.shard_events.ShardPayload
+        hikari.events.shard_events.ShardPayloadEvent
             The parsed shard payload event object.
         """
 
diff --git a/hikari/api/event_manager.py b/hikari/api/event_manager.py
index 90a0c2c2..6208ba1e 100644
--- a/hikari/api/event_manager.py
+++ b/hikari/api/event_manager.py
@@ -161,7 +161,7 @@ async def on_everyone_mentioned(event):
     # For the sake of UX, I will check this at runtime instead and let the
     # user use a static type checker.
     @abc.abstractmethod
-    def subscribe(self, event_type: typing.Type[typing.Any], callback: CallbackT[typing.Any]) -> CallbackT[typing.Any]:
+    def subscribe(self, event_type: typing.Type[typing.Any], callback: CallbackT[typing.Any]) -> None:
         """Subscribe a given callback to a given event type.
 
         Parameters
@@ -189,11 +189,6 @@ async def on_message(event):
         bot.subscribe(MessageCreateEvent, on_message)
         ```
 
-        Returns
-        -------
-        typing.Callable[[T], typing.Coroutine[typing.Any, typing.Any, builtins.None]
-            The event callback that was passed in.
-
         See Also
         --------
         Listen: `hikari.api.event_manager.EventManager.listen`
diff --git a/hikari/api/rest.py b/hikari/api/rest.py
index be50ad76..841c661f 100644
--- a/hikari/api/rest.py
+++ b/hikari/api/rest.py
@@ -61,9 +61,20 @@ class TokenStrategy(abc.ABC):
 
     __slots__: typing.Sequence[str] = ()
 
+    @property
+    @abc.abstractmethod
+    def token_type(self) -> typing.Union[applications.TokenType, str]:
+        """Type of token this strategy returns.
+
+        Returns
+        -------
+        typing.Union[hikari.applications.TokenType, builtins.str]
+            The type of token this strategy returns.
+        """
+
     @abc.abstractmethod
     async def acquire(self, client: RESTClient) -> str:
-        """Acquire an authorization token (with the prefix).
+        """Acquire an authorization token (including the prefix).
 
         Returns
         -------
@@ -98,6 +109,20 @@ class RESTClient(traits.NetworkSettingsAware, abc.ABC):
 
     __slots__: typing.Sequence[str] = ()
 
+    @property
+    @abc.abstractmethod
+    def token_type(self) -> typing.Union[str, applications.TokenType, None]:
+        """Type of token this client is using for most requests.
+
+        Returns
+        -------
+        typing.Union[builtins.str, hikari.applications.TokenType, builtins.None]
+            The type of token this client is using for most requests.
+
+            If this is `builtins.None` then this client will likely only work
+            for some endpoints such as public and webhook ones.
+        """
+
     @abc.abstractmethod
     async def close(self) -> None:
         """Close the client session."""
@@ -1028,6 +1053,7 @@ async def create_message(
         content: undefined.UndefinedOr[typing.Any] = undefined.UNDEFINED,
         *,
         embed: undefined.UndefinedOr[embeds_.Embed] = undefined.UNDEFINED,
+        embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED,
         attachment: undefined.UndefinedOr[files.Resourceish] = undefined.UNDEFINED,
         attachments: undefined.UndefinedOr[typing.Sequence[files.Resourceish]] = undefined.UNDEFINED,
         tts: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
@@ -1054,9 +1080,9 @@ async def create_message(
             in the content. Any other value here will be cast to a
             `builtins.str`.
 
-            If this is a `hikari.embeds.Embed` and no `embed` kwarg is
-            provided, then this will instead update the embed. This allows for
-            simpler syntax when sending an embed alone.
+            If this is a `hikari.embeds.Embed` and no `embed` nor `embeds` kwarg
+            is provided, then this will instead update the embed. This allows
+            for simpler syntax when sending an embed alone.
 
             Likewise, if this is a `hikari.files.Resource`, then the
             content is instead treated as an attachment if no `attachment` and
@@ -1066,6 +1092,8 @@ async def create_message(
         ----------------
         embed : hikari.undefined.UndefinedOr[hikari.embeds.Embed]
             If provided, the message embed.
+        embeds : hikari.undefined.UndefinedOr[typing.Sequence[hikari.embeds.Embed]]
+            If provided, the message embeds.
         attachment : hikari.undefined.UndefinedOr[hikari.files.Resourceish],
             If provided, the message attachment. This can be a resource,
             or string of a path on your computer or a URL.
@@ -1152,10 +1180,8 @@ async def create_message(
             being empty with no attachments or embeds; messages with more than
             2000 characters in them, embeds that exceed one of the many embed
             limits; too many attachments; attachments that are too large;
-            invalid image URLs in embeds; users in `user_mentions` not being
-            mentioned in the message content; roles in `role_mentions` not
-            being mentioned in the message content; if `reply` is
-            not found or not in the same channel as `channel`.
+            invalid image URLs in embeds; if `reply` is ot found or not in the
+            same channel as `channel`.
         hikari.errors.UnauthorizedError
             If you are unauthorized to make the request (invalid/missing token).
         hikari.errors.ForbiddenError
@@ -1176,10 +1202,6 @@ async def create_message(
             nature, and will trigger this exception if they occur.
         hikari.errors.InternalServerError
             If an internal error occurs on Discord while handling the request.
-
-        !!! warning
-            You are expected to make a connection to the gateway and identify
-            once before being able to use this endpoint for a bot.
         """  # noqa: E501 - Line too long
 
     @abc.abstractmethod
@@ -1238,6 +1260,7 @@ async def edit_message(
         content: undefined.UndefinedOr[typing.Any] = undefined.UNDEFINED,
         *,
         embed: undefined.UndefinedNoneOr[embeds_.Embed] = undefined.UNDEFINED,
+        embeds: undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED,
         attachment: undefined.UndefinedOr[files.Resourceish] = undefined.UNDEFINED,
         attachments: undefined.UndefinedOr[typing.Sequence[files.Resourceish]] = undefined.UNDEFINED,
         replace_attachments: bool = False,
@@ -1268,8 +1291,9 @@ async def edit_message(
 
             Any other value will be cast to a `builtins.str` before sending.
 
-            If this is a `hikari.embeds.Embed` and no `embed` kwarg is
-            provided or if this is a `hikari.files.Resourceish` and neither the
+            If this is a `hikari.embeds.Embed` and neither the `embed` or
+            `embeds` kwargs are provided or if this is a
+            `hikari.files.Resourceish` and neither the
             `attachment` or `attachments` kwargs are provided, the values will
             be overwritten. This allows for simpler syntax when sending an
             embed or an attachment alone.
@@ -1278,10 +1302,16 @@ async def edit_message(
         ----------------
         embed : hikari.undefined.UndefinedNoneOr[hikari.embeds.Embed]
             If provided, the embed to set on the message. If
-            `hikari.undefined.UNDEFINED`, the previous embed, if
-            present, is not changed. If this is `builtins.None`, then the embed
-            is removed, if present. Otherwise, the new embed that was provided
-            will be used as the replacement.
+            `hikari.undefined.UNDEFINED`, the previous embed(s) are not changed.
+            If this is `builtins.None` then any present embeds are removed.
+            Otherwise, the new embed that was provided will be used as the
+            replacement.
+        embeds : hikari.undefined.UndefinedNoneOr[typing.Sequence[hikari.embeds.Embed]]
+            If provided, the embeds to set on the message. If
+            `hikari.undefined.UNDEFINED`, the previous embed(s) are not changed.
+            If this is `builtins.None` then any present embeds are removed.
+            Otherwise, the new embeds that were provided will be used as the
+            replacement.
         attachment : hikari.undefined.UndefinedOr[hikari.files.Resourceish]
             If provided, the attachment to set on the message. If
             `hikari.undefined.UNDEFINED`, the previous attachment, if
@@ -1377,9 +1407,7 @@ async def edit_message(
             This may be raised in several discrete situations, such as messages
             being empty with no embeds; messages with more than 2000 characters
             in them, embeds that exceed one of the many embed
-            limits; invalid image URLs in embeds; users in `user_mentions` not
-            being mentioned in the message content; roles in `role_mentions` not
-            being mentioned in the message content.
+            limits; invalid image URLs in embeds.
         hikari.errors.UnauthorizedError
             If you are unauthorized to make the request (invalid/missing token).
         hikari.errors.ForbiddenError
@@ -2141,7 +2169,7 @@ async def execute_webhook(
         ----------------
         embed : hikari.undefined.UndefinedOr[hikari.embeds.Embed]
             If provided, the message embed.
-        embeds : hikari.undefined.UndefinedOr[hikari.embeds.Embed]
+        embeds : hikari.undefined.UndefinedOr[typing.Sequence[hikari.embeds.Embed]]
             If provided, the message embeds.
         attachment : hikari.undefined.UndefinedOr[hikari.files.Resourceish],
             If provided, the message attachment. This can be a resource,
@@ -2223,9 +2251,7 @@ async def execute_webhook(
             being empty with no attachments or embeds; messages with more than
             2000 characters in them, embeds that exceed one of the many embed
             limits; too many attachments; attachments that are too large;
-            invalid image URLs in embeds; users in `user_mentions` not being
-            mentioned in the message content; roles in `role_mentions` not
-            being mentioned in the message content.
+            invalid image URLs in embeds.
         hikari.errors.UnauthorizedError
             If you are unauthorized to make the request (invalid/missing token).
         hikari.errors.NotFoundError
@@ -2342,16 +2368,16 @@ async def edit_webhook_message(
         ----------------
         embed : hikari.undefined.UndefinedNoneOr[hikari.embeds.Embed]
             If provided, the embed to set on the message. If
-            `hikari.undefined.UNDEFINED`, the previous embed, if
-            present, is not changed. If this is `builtins.None`, then the embed
-            is removed, if present. Otherwise, the new embed that was provided
-            will be used as the replacement.
-        embeds : hikari.undefined.UndefinedNoneOr[hikari.embeds.Embed]
+            `hikari.undefined.UNDEFINED`, the previous embed(s) are not changed.
+            If this is `builtins.None` then any present embeds are removed.
+            Otherwise, the new embed that was provided will be used as the
+            replacement.
+        embeds : hikari.undefined.UndefinedNoneOr[typing.Sequence[hikari.embeds.Embed]]
             If provided, the embeds to set on the message. If
-            `hikari.undefined.UNDEFINED`, the previous embeds if
-            present are not changed. If this is `builtins.None`, then the embeds
-            are removed ,if present. Otherwise, the new embeds that were provided
-            will be used as the replacement.
+            `hikari.undefined.UNDEFINED`, the previous embed(s) are not changed.
+            If this is `builtins.None` then any present embeds are removed.
+            Otherwise, the new embeds that were provided will be used as the
+            replacement.
         attachment : hikari.undefined.UndefinedOr[hikari.files.Resourceish]
             If provided, the attachment to set on the message. If
             `hikari.undefined.UNDEFINED`, the previous attachment, if
@@ -2432,9 +2458,7 @@ async def edit_webhook_message(
             being empty with no attachments or embeds; messages with more than
             2000 characters in them, embeds that exceed one of the many embed
             limits; too many attachments; attachments that are too large;
-            invalid image URLs in embeds; users in `user_mentions` not being
-            mentioned in the message content; roles in `role_mentions` not
-            being mentioned in the message content.
+            invalid image URLs in embeds.
         hikari.errors.UnauthorizedError
             If you are unauthorized to make the request (invalid/missing token).
         hikari.errors.NotFoundError
diff --git a/hikari/applications.py b/hikari/applications.py
index 96fd149a..3596842c 100644
--- a/hikari/applications.py
+++ b/hikari/applications.py
@@ -59,7 +59,6 @@
 if typing.TYPE_CHECKING:
     import datetime
 
-    from hikari import channels
     from hikari import files
     from hikari import permissions as permissions_
     from hikari import traits
@@ -347,10 +346,6 @@ def flags(self) -> users.UserFlag:
     def id(self) -> snowflakes.Snowflake:
         return self.user.id
 
-    @id.setter
-    def id(self, value: snowflakes.Snowflake) -> typing.NoReturn:
-        raise TypeError("Cannot mutate the ID of a member")
-
     @property
     def is_bot(self) -> bool:
         return self.user.is_bot
@@ -367,9 +362,6 @@ def mention(self) -> str:
     def username(self) -> str:
         return self.user.username
 
-    async def fetch_dm_channel(self) -> channels.DMChannel:
-        return await self.user.fetch_dm_channel()
-
     def __str__(self) -> str:
         return str(self.user)
 
diff --git a/hikari/channels.py b/hikari/channels.py
index 52a2f531..f1f2d73a 100644
--- a/hikari/channels.py
+++ b/hikari/channels.py
@@ -61,7 +61,7 @@
 if typing.TYPE_CHECKING:
     import datetime
 
-    from hikari import embeds
+    from hikari import embeds as embeds_
     from hikari import files
     from hikari import guilds
     from hikari import iterators
@@ -404,7 +404,8 @@ async def send(
         self,
         content: undefined.UndefinedOr[typing.Any] = undefined.UNDEFINED,
         *,
-        embed: undefined.UndefinedOr[embeds.Embed] = undefined.UNDEFINED,
+        embed: undefined.UndefinedOr[embeds_.Embed] = undefined.UNDEFINED,
+        embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED,
         attachment: undefined.UndefinedOr[files.Resourceish] = undefined.UNDEFINED,
         attachments: undefined.UndefinedOr[typing.Sequence[files.Resourceish]] = undefined.UNDEFINED,
         nonce: undefined.UndefinedOr[str] = undefined.UNDEFINED,
@@ -429,9 +430,9 @@ async def send(
             in the content. Any other value here will be cast to a
             `builtins.str`.
 
-            If this is a `hikari.embeds.Embed` and no `embed` kwarg is
-            provided, then this will instead update the embed. This allows for
-            simpler syntax when sending an embed alone.
+            If this is a `hikari.embeds.Embed` and no `embed` nor `embeds` kwarg
+            is provided, then this will instead update the embed. This allows
+            for simpler syntax when sending an embed alone.
 
             Likewise, if this is a `hikari.files.Resource`, then the
             content is instead treated as an attachment if no `attachment` and
@@ -441,6 +442,8 @@ async def send(
         ----------------
         embed : hikari.undefined.UndefinedOr[hikari.embeds.Embed]
             If provided, the message embed.
+        embeds : hikari.undefined.UndefinedOr[typing.Sequence[hikari.embeds.Embed]]
+            If provided, the message embeds.
         attachment : hikari.undefined.UndefinedOr[hikari.files.Resourceish],
             If provided, the message attachment. This can be a resource,
             or string of a path on your computer or a URL.
@@ -516,10 +519,8 @@ async def send(
             being empty with no attachments or embeds; messages with more than
             2000 characters in them, embeds that exceed one of the many embed
             limits; too many attachments; attachments that are too large;
-            invalid image URLs in embeds; users in `user_mentions` not being
-            mentioned in the message content; roles in `role_mentions` not
-            being mentioned in the message content; `reply` not found
-            or not in the same channel.
+            invalid image URLs in embeds; `reply` not found or not in the same
+            channel.
         hikari.errors.UnauthorizedError
             If you are unauthorized to make the request (invalid/missing token).
         hikari.errors.ForbiddenError
@@ -533,15 +534,12 @@ async def send(
             `role_mentions` or `user_mentions`.
         builtins.TypeError
             If both `attachment` and `attachments` are specified.
-
-        !!! warning
-            You are expected to make a connection to the gateway and identify
-            once before being able to use this endpoint for a bot.
         """  # noqa: E501 - Line too long
         return await self.app.rest.create_message(
             channel=self.id,
             content=content,
             embed=embed,
+            embeds=embeds,
             attachment=attachment,
             attachments=attachments,
             nonce=nonce,
@@ -717,18 +715,29 @@ class GuildChannel(PartialChannel):
     If no parent category is set for the channel, this will be `builtins.None`.
     """
 
+    @property
+    def mention(self) -> str:
+        """Return a raw mention string for the guild channel.
+
+        !!! note
+            As of writing, GuildCategory channels are a special case
+            for this and mentions of them will not resolve as clickable.
+
+        Returns
+        -------
+        builtins.str
+            The mention string to use.
+        """
+        return f"<#{self.id}>"
+
     @property
     def shard_id(self) -> typing.Optional[int]:
         """Return the shard ID for the shard.
 
         This may be `builtins.None` if the shard count is not known.
         """
-        try:
-            shard_count = getattr(self.app, "shard_count")
-            assert isinstance(shard_count, int), f"shard_count attr was expected to be int, but got {shard_count}"
-            return snowflakes.calculate_shard_id(shard_count, self.guild_id)
-        except (TypeError, AttributeError, NameError):
-            pass
+        if isinstance(self.app, traits.ShardAware):
+            return snowflakes.calculate_shard_id(self.app, self.guild_id)
 
         return None
 
diff --git a/hikari/config.py b/hikari/config.py
index 25ab27a8..c57d3269 100644
--- a/hikari/config.py
+++ b/hikari/config.py
@@ -428,7 +428,21 @@ class CacheComponents(enums.Flag):
     MESSAGES = 1 << 8
     """Enables the messages cache."""
 
-    ALL = GUILDS | GUILD_CHANNELS | MEMBERS | ROLES | INVITES | EMOJIS | PRESENCES | VOICE_STATES | MESSAGES
+    DM_CHANNEL_IDS = 1 << 10
+    """Enables the DM channel IDs cache."""
+
+    ALL = (
+        GUILDS
+        | GUILD_CHANNELS
+        | MEMBERS
+        | ROLES
+        | INVITES
+        | EMOJIS
+        | PRESENCES
+        | VOICE_STATES
+        | MESSAGES
+        | DM_CHANNEL_IDS
+    )
     """Fully enables the cache."""
 
 
diff --git a/hikari/embeds.py b/hikari/embeds.py
index aa0511bb..c8a6a3ee 100644
--- a/hikari/embeds.py
+++ b/hikari/embeds.py
@@ -295,7 +295,7 @@ class Embed:
     )
 
     # Don't document this.
-    __pdoc__: typing.Final[typing.ClassVar[typing.Mapping[str, typing.Any]]] = {"from_received_embed": False}
+    __pdoc__: typing.ClassVar[typing.Mapping[str, typing.Any]] = {"from_received_embed": False}
 
     @classmethod
     def from_received_embed(
@@ -733,13 +733,8 @@ def set_author(
         if name is None and url is None and icon is None:
             self._author = None
         else:
-            self._author = EmbedAuthor()
-            self._author.name = name
-            self._author.url = url
-            if icon is not None:
-                self._author.icon = EmbedResourceWithProxy(resource=files.ensure_resource(icon))
-            else:
-                self._author.icon = None
+            real_icon = EmbedResourceWithProxy(resource=files.ensure_resource(icon)) if icon is not None else None
+            self._author = EmbedAuthor(name=name, url=url, icon=real_icon)
         return self
 
     def set_footer(self, *, text: typing.Optional[str], icon: typing.Optional[files.Resourceish] = None) -> Embed:
@@ -787,12 +782,8 @@ def set_footer(self, *, text: typing.Optional[str], icon: typing.Optional[files.
 
             self._footer = None
         else:
-            self._footer = EmbedFooter()
-            self._footer.text = text
-            if icon is not None:
-                self._footer.icon = EmbedResourceWithProxy(resource=files.ensure_resource(icon))
-            else:
-                self._footer.icon = None
+            real_icon = EmbedResourceWithProxy(resource=files.ensure_resource(icon)) if icon is not None else None
+            self._footer = EmbedFooter(icon=real_icon, text=text)
         return self
 
     def set_image(self, image: typing.Optional[files.Resourceish] = None, /) -> Embed:
diff --git a/hikari/emojis.py b/hikari/emojis.py
index 7d1ac992..c85b7eac 100644
--- a/hikari/emojis.py
+++ b/hikari/emojis.py
@@ -49,7 +49,6 @@
 _CUSTOM_EMOJI_REGEX: typing.Final[typing.Pattern[str]] = re.compile(r"<(?P<flags>[^:]*):(?P<name>[^:]*):(?P<id>\d+)>")
 
 
-@attr.define(hash=True, kw_only=True, weakref_slot=False)
 class Emoji(files.WebResource, abc.ABC):
     """Base class for all emojis.
 
@@ -59,6 +58,8 @@ class Emoji(files.WebResource, abc.ABC):
     `hikari.files.WebResource` would achieve this.
     """
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def name(self) -> typing.Optional[str]:
diff --git a/hikari/errors.py b/hikari/errors.py
index 4b2ef405..68942432 100644
--- a/hikari/errors.py
+++ b/hikari/errors.py
@@ -227,10 +227,7 @@ class HTTPError(HikariError):
 
 @typing.final
 class RESTErrorCode(int, enums.Enum):
-    """Error codes provided as further info on errors returned by the REST API."""
-
-    UNKNOWN_ERROR = -1
-    """Error is not known."""
+    """Non-exhaustive enum of error codes provided as further info on errors returned by the REST API."""
 
     GENERAL_ERROR = 0
     """A general error, no further info provided."""
@@ -286,6 +283,9 @@ class RESTErrorCode(int, enums.Enum):
     WRITE_LIMIT_HIT = 20_028
     """The global write limit on a channel has been hit."""
 
+    DISALLOWED_WORDS_IN_PUBLIC_STAGES = 20_031
+    """The stage channel contains disallowed words for public stages."""
+
     MAXIMUM_GUILDS = 30_001
     """Maximum number of guilds reached (100)."""
 
diff --git a/hikari/events/__init__.pyi b/hikari/events/__init__.pyi
new file mode 100644
index 00000000..be224d54
--- /dev/null
+++ b/hikari/events/__init__.pyi
@@ -0,0 +1,14 @@
+# This file was generated by `nox -s generate-stubs`. Do not manually edit this file!
+
+from hikari.events.channel_events import *
+from hikari.events.guild_events import *
+from hikari.events.lifetime_events import *
+from hikari.events.member_events import *
+from hikari.events.message_events import *
+from hikari.events.reaction_events import *
+from hikari.events.role_events import *
+from hikari.events.shard_events import *
+from hikari.events.typing_events import *
+from hikari.events.user_events import *
+from hikari.events.voice_events import *
+from hikari.events.base_events import Event as Event, ExceptionEvent as ExceptionEvent
diff --git a/hikari/events/base_events.py b/hikari/events/base_events.py
index 9d562d41..1b3f7a3c 100644
--- a/hikari/events/base_events.py
+++ b/hikari/events/base_events.py
@@ -52,10 +52,11 @@
 NO_RECURSIVE_THROW_ATTR: typing.Final[str] = "___norecursivethrow___"
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 class Event(abc.ABC):
     """Base event type that all Hikari events should subclass."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def app(self) -> traits.RESTAware:
diff --git a/hikari/events/channel_events.py b/hikari/events/channel_events.py
index 5b13ba65..082c1e18 100644
--- a/hikari/events/channel_events.py
+++ b/hikari/events/channel_events.py
@@ -69,10 +69,11 @@
 
 
 @base_events.requires_intents(intents.Intents.GUILDS, intents.Intents.DM_MESSAGES)
-@attr.define(kw_only=True, weakref_slot=False)
 class ChannelEvent(shard_events.ShardEvent, abc.ABC):
     """Event base for any channel-bound event in guilds or private messages."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def channel_id(self) -> snowflakes.Snowflake:
@@ -124,10 +125,11 @@ async def fetch_channel(self) -> channels.PartialChannel:
 
 
 @base_events.requires_intents(intents.Intents.GUILDS)
-@attr.define(kw_only=True, weakref_slot=False)
 class GuildChannelEvent(ChannelEvent, abc.ABC):
     """Event base for any channel-bound event in guilds."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def guild_id(self) -> snowflakes.Snowflake:
@@ -246,10 +248,11 @@ async def fetch_channel(self) -> channels.GuildChannel:
         return channel
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 class DMChannelEvent(ChannelEvent, abc.ABC):
     """Event base for any channel-bound event in private messages."""
 
+    __slots__: typing.Sequence[str] = ()
+
     async def fetch_channel(self) -> channels.PrivateChannel:
         """Perform an API call to fetch the details about this channel.
 
@@ -292,10 +295,11 @@ async def fetch_channel(self) -> channels.PrivateChannel:
 
 
 @base_events.requires_intents(intents.Intents.GUILDS, intents.Intents.DM_MESSAGES)
-@attr.define(kw_only=True, weakref_slot=False)
 class ChannelCreateEvent(ChannelEvent, abc.ABC):
     """Base event for any channel being created."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def channel(self) -> channels.PartialChannel:
@@ -341,10 +345,11 @@ def guild_id(self) -> snowflakes.Snowflake:
 
 
 @base_events.requires_intents(intents.Intents.GUILDS, intents.Intents.DM_MESSAGES)
-@attr.define(kw_only=True, weakref_slot=False)
 class ChannelUpdateEvent(ChannelEvent, abc.ABC):
     """Base event for any channel being updated."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def channel(self) -> channels.PartialChannel:
@@ -396,10 +401,11 @@ def guild_id(self) -> snowflakes.Snowflake:
 
 
 @base_events.requires_intents(intents.Intents.GUILDS, intents.Intents.DM_MESSAGES)
-@attr.define(kw_only=True, weakref_slot=False)
 class ChannelDeleteEvent(ChannelEvent, abc.ABC):
     """Base event for any channel being deleted."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def channel(self) -> channels.PartialChannel:
@@ -455,10 +461,11 @@ async def fetch_channel(self) -> typing.NoReturn:
 
 
 @base_events.requires_intents(intents.Intents.DM_MESSAGES, intents.Intents.GUILDS)
-@attr.define(kw_only=True, weakref_slot=False)
 class PinsUpdateEvent(ChannelEvent, abc.ABC):
     """Base event fired when a message is pinned/unpinned in a channel."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def last_pin_timestamp(self) -> typing.Optional[datetime.datetime]:
@@ -628,10 +635,11 @@ async def fetch_channel(self) -> channels.DMChannel:
 
 
 @base_events.requires_intents(intents.Intents.GUILD_INVITES)
-@attr.define(kw_only=True, weakref_slot=False)
 class InviteEvent(GuildChannelEvent, abc.ABC):
     """Base event type for guild invite updates."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def code(self) -> str:
diff --git a/hikari/events/guild_events.py b/hikari/events/guild_events.py
index 4576873c..de0169b5 100644
--- a/hikari/events/guild_events.py
+++ b/hikari/events/guild_events.py
@@ -64,13 +64,14 @@
     from hikari.api import shard as gateway_shard
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(
     intents.Intents.GUILDS, intents.Intents.GUILD_BANS, intents.Intents.GUILD_EMOJIS, intents.Intents.GUILD_PRESENCES
 )
 class GuildEvent(shard_events.ShardEvent, abc.ABC):
     """Event base for any guild-bound event."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def guild_id(self) -> snowflakes.Snowflake:
@@ -119,7 +120,6 @@ async def fetch_guild_preview(self) -> guilds.GuildPreview:
         return await self.app.rest.fetch_guild_preview(self.guild_id)
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.GUILDS)
 class GuildVisibilityEvent(GuildEvent, abc.ABC):
     """Event base for any event that changes the visibility of a guild.
@@ -130,6 +130,8 @@ class GuildVisibilityEvent(GuildEvent, abc.ABC):
     the user joins a new guild.
     """
 
+    __slots__: typing.Sequence[str] = ()
+
 
 @attr_extensions.with_copy
 @attr.define(kw_only=True, weakref_slot=False)
@@ -325,11 +327,12 @@ def guild_id(self) -> snowflakes.Snowflake:
         return self.guild.id
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.GUILD_BANS)
 class BanEvent(GuildEvent, abc.ABC):
     """Event base for any guild ban or unban."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def user(self) -> users.User:
@@ -443,11 +446,12 @@ async def fetch_emojis(self) -> typing.Sequence[emojis_.KnownCustomEmoji]:
         return await self.app.rest.fetch_guild_emojis(self.guild_id)
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.GUILD_INTEGRATIONS)
 class IntegrationEvent(GuildEvent, abc.ABC):
     """Event base for any integration related events."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def application_id(self) -> typing.Optional[snowflakes.Snowflake]:
diff --git a/hikari/events/member_events.py b/hikari/events/member_events.py
index 97bbe52f..098636ad 100644
--- a/hikari/events/member_events.py
+++ b/hikari/events/member_events.py
@@ -48,11 +48,12 @@
     from hikari.api import shard as gateway_shard
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.GUILD_MEMBERS)
 class MemberEvent(shard_events.ShardEvent, abc.ABC):
     """Event base for any events that concern guild members."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def guild_id(self) -> snowflakes.Snowflake:
diff --git a/hikari/events/message_events.py b/hikari/events/message_events.py
index 930c2a09..5a933132 100644
--- a/hikari/events/message_events.py
+++ b/hikari/events/message_events.py
@@ -59,11 +59,12 @@
     from hikari.api import shard as shard_
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.DM_MESSAGES, intents.Intents.GUILD_MESSAGES)
 class MessageEvent(shard_events.ShardEvent, abc.ABC):
     """Any event that concerns manipulation of messages."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def channel_id(self) -> snowflakes.Snowflake:
@@ -87,11 +88,12 @@ def message_id(self) -> snowflakes.Snowflake:
         """
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.DM_MESSAGES, intents.Intents.GUILD_MESSAGES)
 class MessageCreateEvent(MessageEvent, abc.ABC):
     """Event that is fired when a message is created."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     def author(self) -> users.User:
         """User that sent the message.
@@ -220,17 +222,27 @@ class GuildMessageCreateEvent(MessageCreateEvent):
     # <<inherited docstring from ShardEvent>>
 
     @property
-    def author(self) -> guilds.Member:
+    def author(self) -> users.User:
+        """User object of the user that sent the message.
+
+        Returns
+        -------
+        hikari.users.User
+            The user object of the user that sent the message.
+        """
+        return self.message.author
+
+    @property
+    def member(self) -> typing.Optional[guilds.Member]:
         """Member object of the user that sent the message.
 
         Returns
         -------
-        hikari.guilds.Member
-            The member object of the user that sent the message.
+        typing.Optional[hikari.guilds.Member]
+            The member object of the user that sent the message or
+            `builtins.None` if sent by a webhook.
         """
-        member = self.message.member
-        assert member is not None, "no member given for guild message create event!"
-        return member
+        return self.message.member
 
     @property
     def channel(self) -> typing.Union[None, channels.GuildTextChannel, channels.GuildNewsChannel]:
@@ -304,7 +316,6 @@ class DMMessageCreateEvent(MessageCreateEvent):
     # <<inherited docstring from ShardEvent>>
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.DM_MESSAGES, intents.Intents.GUILD_MESSAGES)
 class MessageUpdateEvent(MessageEvent, abc.ABC):
     """Event that is fired when a message is updated.
@@ -314,6 +325,8 @@ class MessageUpdateEvent(MessageEvent, abc.ABC):
         due to Discord limitations.
     """
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     def author(self) -> typing.Optional[users.User]:
         """User that sent the message.
@@ -476,18 +489,34 @@ class GuildMessageUpdateEvent(MessageUpdateEvent):
 
     @property
     def author(self) -> typing.Optional[users.User]:
-        """Member or user that sent the message.
+        """User that sent the message.
 
         Returns
         -------
         typing.Union[builtins.None, hikari.users.User, hikari.guilds.Member]
-            The user that sent the message. If the member is cached
+            The user that sent the message.
+
+            This will be `builtins.None` in some cases, such as when Discord
+            updates a message with an embed for a URL preview or if the message
+            was sent by a webhook.
+        """
+        return self.message.author
+
+    @property
+    def member(self) -> typing.Optional[guilds.Member]:
+        """Member that sent the message.
+
+        Returns
+        -------
+        typing.Optional[hikari.guilds.Member]
+            The member that sent the message. If the member is cached
             (the intents are enabled), then this will be the corresponding
             member object instead (which is a specialization of the
             user object you should otherwise expect).
 
             This will be `builtins.None` in some cases, such as when Discord
-            updates a message with an embed for a URL preview.
+            updates a message with an embed for a URL preview or if the message
+            was sent by a webhook.
         """
         member = self.message.member
         if member is not None:
@@ -501,7 +530,7 @@ def author(self) -> typing.Optional[users.User]:
             if member is not None:
                 return member
 
-        return author
+        return None
 
     @property
     def channel(self) -> typing.Union[None, channels.GuildTextChannel, channels.GuildNewsChannel]:
@@ -583,7 +612,6 @@ class DMMessageUpdateEvent(MessageUpdateEvent):
     # <<inherited docstring from ShardEvent>>
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.GUILD_MESSAGES, intents.Intents.DM_MESSAGES)
 class MessageDeleteEvent(MessageEvent, abc.ABC):
     """Special event that is triggered when one or more messages get deleted.
@@ -596,6 +624,8 @@ class MessageDeleteEvent(MessageEvent, abc.ABC):
     `is_bulk` attribute.
     """
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     def message_id(self) -> snowflakes.Snowflake:
         """Get the ID of the first deleted message.
diff --git a/hikari/events/reaction_events.py b/hikari/events/reaction_events.py
index 7796ed83..b0ff4d33 100644
--- a/hikari/events/reaction_events.py
+++ b/hikari/events/reaction_events.py
@@ -60,11 +60,12 @@
     from hikari.api import shard as gateway_shard
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.GUILD_MESSAGE_REACTIONS, intents.Intents.DM_MESSAGE_REACTIONS)
 class ReactionEvent(shard_events.ShardEvent, abc.ABC):
     """Event base for any message reaction event."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def channel_id(self) -> snowflakes.Snowflake:
@@ -88,11 +89,12 @@ def message_id(self) -> snowflakes.Snowflake:
         """
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.GUILD_MESSAGE_REACTIONS)
 class GuildReactionEvent(ReactionEvent, abc.ABC):
     """Event base for any reaction-bound event in guild messages."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def guild_id(self) -> snowflakes.Snowflake:
@@ -105,17 +107,19 @@ def guild_id(self) -> snowflakes.Snowflake:
         """
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.DM_MESSAGE_REACTIONS)
 class DMReactionEvent(ReactionEvent, abc.ABC):
     """Event base for any reaction-bound event in private messages."""
 
+    __slots__: typing.Sequence[str] = ()
+
 
-@attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.GUILD_MESSAGE_REACTIONS, intents.Intents.DM_MESSAGE_REACTIONS)
 class ReactionAddEvent(ReactionEvent, abc.ABC):
     """Event base for any reaction that is added to a message."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def user_id(self) -> snowflakes.Snowflake:
@@ -140,11 +144,12 @@ def emoji(self) -> emojis.Emoji:
         """
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.GUILD_MESSAGE_REACTIONS, intents.Intents.DM_MESSAGE_REACTIONS)
 class ReactionDeleteEvent(ReactionEvent, abc.ABC):
     """Event base for any single reaction that is removed from a message."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def user_id(self) -> snowflakes.Snowflake:
@@ -170,17 +175,19 @@ def emoji(self) -> emojis.Emoji:
         """
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.GUILD_MESSAGE_REACTIONS, intents.Intents.DM_MESSAGE_REACTIONS)
 class ReactionDeleteAllEvent(ReactionEvent, abc.ABC):
     """Event base fired when all reactions are removed from a message."""
 
+    __slots__: typing.Sequence[str] = ()
+
 
-@attr.define(kw_only=True, weakref_slot=False)
 @base_events.requires_intents(intents.Intents.GUILD_MESSAGE_REACTIONS, intents.Intents.DM_MESSAGE_REACTIONS)
 class ReactionDeleteEmojiEvent(ReactionEvent, abc.ABC):
     """Event base fired when all reactions are removed for one emoji."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def emoji(self) -> emojis.Emoji:
diff --git a/hikari/events/role_events.py b/hikari/events/role_events.py
index c0888e7e..8447a547 100644
--- a/hikari/events/role_events.py
+++ b/hikari/events/role_events.py
@@ -51,6 +51,8 @@
 class RoleEvent(shard_events.ShardEvent, abc.ABC):
     """Event base for any event that involves guild roles."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def guild_id(self) -> snowflakes.Snowflake:
diff --git a/hikari/events/shard_events.py b/hikari/events/shard_events.py
index a25e103e..5ee76d25 100644
--- a/hikari/events/shard_events.py
+++ b/hikari/events/shard_events.py
@@ -26,7 +26,7 @@
 
 __all__: typing.List[str] = [
     "ShardEvent",
-    "ShardPayload",
+    "ShardPayloadEvent",
     "ShardStateEvent",
     "ShardConnectedEvent",
     "ShardDisconnectedEvent",
@@ -57,6 +57,8 @@
 class ShardEvent(base_events.Event, abc.ABC):
     """Base class for any event that was shard-specific."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def shard(self) -> gateway_shard.GatewayShard:
@@ -71,7 +73,7 @@ def shard(self) -> gateway_shard.GatewayShard:
 
 @attr_extensions.with_copy
 @attr.define(kw_only=True, weakref_slot=False)
-class ShardPayload(ShardEvent):
+class ShardPayloadEvent(ShardEvent):
     """Event fired for most shard events with their raw payload.
 
     !!! note
@@ -98,6 +100,8 @@ class ShardStateEvent(ShardEvent, abc.ABC):
     This currently wraps connection/disconnection/ready/resumed events only.
     """
 
+    __slots__: typing.Sequence[str] = ()
+
 
 @attr_extensions.with_copy
 @attr.define(kw_only=True, weakref_slot=False)
diff --git a/hikari/events/typing_events.py b/hikari/events/typing_events.py
index bf238019..ff5f95c7 100644
--- a/hikari/events/typing_events.py
+++ b/hikari/events/typing_events.py
@@ -55,6 +55,8 @@
 class TypingEvent(shard_events.ShardEvent, abc.ABC):
     """Base event fired when a user begins typing in a channel."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def channel_id(self) -> snowflakes.Snowflake:
diff --git a/hikari/events/voice_events.py b/hikari/events/voice_events.py
index b0c90987..249843a2 100644
--- a/hikari/events/voice_events.py
+++ b/hikari/events/voice_events.py
@@ -47,10 +47,11 @@
     from hikari.api import shard as gateway_shard
 
 
-@attr.define(kw_only=True, weakref_slot=False)
 class VoiceEvent(shard_events.ShardEvent, abc.ABC):
     """Base for any voice-related event."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def guild_id(self) -> snowflakes.Snowflake:
diff --git a/hikari/guilds.py b/hikari/guilds.py
index ca3f1349..c5c84638 100644
--- a/hikari/guilds.py
+++ b/hikari/guilds.py
@@ -383,10 +383,6 @@ def flags(self) -> users.UserFlag:
     def id(self) -> snowflakes.Snowflake:
         return self.user.id
 
-    @id.setter
-    def id(self, value: snowflakes.Snowflake) -> None:
-        raise TypeError("Cannot mutate the ID of a member")
-
     @property
     def is_bot(self) -> bool:
         return self.user.is_bot
@@ -510,9 +506,6 @@ async def fetch_self(self) -> Member:
         """
         return await self.user.app.rest.fetch_member(self.guild_id, self.user.id)
 
-    async def fetch_dm_channel(self) -> channels_.DMChannel:
-        return await self.user.fetch_dm_channel()
-
     async def ban(
         self,
         *,
@@ -584,6 +577,17 @@ class PartialRole(snowflakes.Unique):
     name: str = attr.field(eq=False, hash=False, repr=True)
     """The role's name."""
 
+    @property
+    def mention(self) -> str:
+        """Return a raw mention string for the role.
+
+        Returns
+        -------
+        builtins.str
+            The mention string to use.
+        """
+        return f"<@&{self.id}>"
+
     def __str__(self) -> str:
         return self.name
 
@@ -1220,6 +1224,61 @@ def discovery_splash_url(self) -> typing.Optional[files.URL]:
         """Discovery splash URL for the guild, if set."""
         return self.make_discovery_splash_url()
 
+    @property
+    def members(self) -> typing.Mapping[snowflakes.Snowflake, Member]:
+        """Get the members cached for the guild.
+
+        typing.Mapping[hikari.snowflakes.Snowflake, Member]
+            A mapping of user IDs to objects of the members cached for the guild.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return {}
+
+        return self.app.cache.get_members_view_for_guild(self.id)
+
+    @property
+    def presences(self) -> typing.Mapping[snowflakes.Snowflake, presences_.MemberPresence]:
+        """Get the presences cached for the guild.
+
+        typing.Mapping[hikari.snowflakes.Snowflake, hikari.presences.MemberPresence]
+            A mapping of user IDs to objects of the presences cached for the
+            guild.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return {}
+
+        return self.app.cache.get_presences_view_for_guild(self.id)
+
+    @property
+    def channels(self) -> typing.Mapping[snowflakes.Snowflake, channels_.GuildChannel]:
+        """Get the channels cached for the guild.
+
+        Returns
+        -------
+        typing.Mapping[hikari.snowflakes.Snowflake, hikari.channels.GuildChannel]
+            A mapping of channel IDs to objects of the channels cached for the
+            guild.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return {}
+
+        return self.app.cache.get_guild_channels_view_for_guild(self.id)
+
+    @property
+    def voice_states(self) -> typing.Mapping[snowflakes.Snowflake, voices_.VoiceState]:
+        """Get the voice states cached for the guild.
+
+        Returns
+        -------
+        typing.Mapping[hikari.snowflakes.Snowflake, hikari.voices.VoiceState]
+            A mapping of user IDs to objects of the voice states cached for the
+            guild.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return {}
+
+        return self.app.cache.get_voice_states_view_for_guild(self.id)
+
     @property
     @abc.abstractmethod
     def emojis(self) -> typing.Mapping[snowflakes.Snowflake, emojis_.KnownCustomEmoji]:
@@ -1346,6 +1405,102 @@ def make_splash_url(self, *, ext: str = "png", size: int = 4096) -> typing.Optio
             file_format=ext,
         )
 
+    def get_channel(
+        self,
+        channel: snowflakes.SnowflakeishOr[channels_.PartialChannel],
+    ) -> typing.Optional[channels_.GuildChannel]:
+        """Get a cached channel that belongs to the guild by it's ID or object.
+
+        Parameters
+        ----------
+        channel : hikari.snowflakes.SnowflakeishOr[hikari.channels.PartialChannel]
+            The object or ID of the guild channel to get from the cache.
+
+        Returns
+        -------
+        typing.Optional[hikari.channels.GuildChannel]
+            The object of the guild channel found in cache or `builtins.None.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return None
+
+        return self.app.cache.get_guild_channel(channel)
+
+    def get_member(self, user: snowflakes.SnowflakeishOr[users.PartialUser]) -> typing.Optional[Member]:
+        """Get a cached member that belongs to the guild by it's user ID or object.
+
+        Parameters
+        ----------
+        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
+            The object or ID of the user to get the cached member for.
+
+        Returns
+        -------
+        typing.Optional[Member]
+            The cached member object if found, else `builtins.None`.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return None
+
+        return self.app.cache.get_member(self.id, user)
+
+    def get_my_member(self) -> typing.Optional[Member]:
+        """Return the cached member for the bot user in this guild, if known.
+
+        Returns
+        -------
+        typing.Optional[Member]
+            The cached member for this guild, or `builtins.None` if not known.
+        """
+        if not isinstance(self.app, traits.ShardAware):
+            return None
+
+        me = self.app.me
+        if me is None:
+            return None
+
+        return self.get_member(me.id)
+
+    def get_presence(
+        self, user: snowflakes.SnowflakeishOr[users.PartialUser]
+    ) -> typing.Optional[presences_.MemberPresence]:
+        """Get a cached presence that belongs to the guild by it's user ID or object.
+
+        Parameters
+        ----------
+        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
+            The object or ID of the user to get the cached presence for.
+
+        Returns
+        -------
+        typing.Optional[hikari.presences.MemberPresence]
+            The cached presence object if found, else `builtins.None`.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return None
+
+        return self.app.cache.get_presence(self.id, user)
+
+    def get_voice_state(
+        self, user: snowflakes.SnowflakeishOr[users.PartialUser]
+    ) -> typing.Optional[voices_.VoiceState]:
+        """Get a cached voice state that belongs to the guild by it's user.
+
+        Parameters
+        ----------
+        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
+            The object or ID of the user to get the cached voice state for.
+
+        Returns
+        -------
+        typing.Optional[hikari.voices.VoiceState]
+            The cached voice state object if found, else `builtins.None`.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return None
+
+        return self.app.cache.get_voice_state(self.id, user)
+
     @abc.abstractmethod
     def get_emoji(
         self, emoji: snowflakes.SnowflakeishOr[emojis_.CustomEmoji]
@@ -1441,21 +1596,6 @@ class GatewayGuild(Guild):
     `builtins.None`.
     """
 
-    @property
-    def channels(self) -> typing.Mapping[snowflakes.Snowflake, channels_.GuildChannel]:
-        """Get the channels cached for the guild.
-
-        Returns
-        -------
-        typing.Mapping[hikari.snowflakes.Snowflake, hikari.channels.GuildChannel]
-            A mapping of channel IDs to objects of the channels cached for the
-            guild.
-        """
-        if not isinstance(self.app, traits.CacheAware):
-            return {}
-
-        return self.app.cache.get_guild_channels_view_for_guild(self.id)
-
     @property
     def emojis(self) -> typing.Mapping[snowflakes.Snowflake, emojis_.KnownCustomEmoji]:
         # <<inherited docstring from Guild>>.
@@ -1464,31 +1604,6 @@ def emojis(self) -> typing.Mapping[snowflakes.Snowflake, emojis_.KnownCustomEmoj
 
         return self.app.cache.get_emojis_view_for_guild(self.id)
 
-    @property
-    def members(self) -> typing.Mapping[snowflakes.Snowflake, Member]:
-        """Get the members cached for the guild.
-
-        typing.Mapping[hikari.snowflakes.Snowflake, Member]
-            A mapping of user IDs to objects of the members cached for the guild.
-        """
-        if not isinstance(self.app, traits.CacheAware):
-            return {}
-
-        return self.app.cache.get_members_view_for_guild(self.id)
-
-    @property
-    def presences(self) -> typing.Mapping[snowflakes.Snowflake, presences_.MemberPresence]:
-        """Get the presences cached for the guild.
-
-        typing.Mapping[hikari.snowflakes.Snowflake, hikari.presences.MemberPresence]
-            A mapping of user IDs to objects of the presences cached for the
-            guild.
-        """
-        if not isinstance(self.app, traits.CacheAware):
-            return {}
-
-        return self.app.cache.get_presences_view_for_guild(self.id)
-
     @property
     def roles(self) -> typing.Mapping[snowflakes.Snowflake, Role]:
         # <<inherited docstring from Guild>>.
@@ -1497,42 +1612,6 @@ def roles(self) -> typing.Mapping[snowflakes.Snowflake, Role]:
 
         return self.app.cache.get_roles_view_for_guild(self.id)
 
-    @property
-    def voice_states(self) -> typing.Mapping[snowflakes.Snowflake, voices_.VoiceState]:
-        """Get the voice states cached for the guild.
-
-        Returns
-        -------
-        typing.Mapping[hikari.snowflakes.Snowflake, hikari.voices.VoiceState]
-            A mapping of user IDs to objects of the voice states cached for the
-            guild.
-        """
-        if not isinstance(self.app, traits.CacheAware):
-            return {}
-
-        return self.app.cache.get_voice_states_view_for_guild(self.id)
-
-    def get_channel(
-        self,
-        channel: snowflakes.SnowflakeishOr[channels_.PartialChannel],
-    ) -> typing.Optional[channels_.GuildChannel]:
-        """Get a cached channel that belongs to the guild by it's ID or object.
-
-        Parameters
-        ----------
-        channel : hikari.snowflakes.SnowflakeishOr[hikari.channels.PartialChannel]
-            The object or ID of the guild channel to get from the cache.
-
-        Returns
-        -------
-        typing.Optional[hikari.channels.GuildChannel]
-            The object of the guild channel found in cache or `builtins.None.
-        """
-        if not isinstance(self.app, traits.CacheAware):
-            return None
-
-        return self.app.cache.get_guild_channel(channel)
-
     def get_emoji(
         self, emoji: snowflakes.SnowflakeishOr[emojis_.CustomEmoji]
     ) -> typing.Optional[emojis_.KnownCustomEmoji]:
@@ -1554,61 +1633,6 @@ def get_emoji(
 
         return self.app.cache.get_emoji(emoji)
 
-    def get_member(self, user: snowflakes.SnowflakeishOr[users.PartialUser]) -> typing.Optional[Member]:
-        """Get a cached member that belongs to the guild by it's user ID or object.
-
-        Parameters
-        ----------
-        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
-            The object or ID of the user to get the cached member for.
-
-        Returns
-        -------
-        typing.Optional[Member]
-            The cached member object if found, else `builtins.None`.
-        """
-        if not isinstance(self.app, traits.CacheAware):
-            return None
-
-        return self.app.cache.get_member(self.id, user)
-
-    def get_my_member(self) -> typing.Optional[Member]:
-        """Return the cached member for the bot user in this guild, if known.
-
-        Returns
-        -------
-        typing.Optional[Member]
-            The cached member for this guild, or `builtins.None` if not known.
-        """
-        if not isinstance(self.app, traits.ShardAware):
-            return None
-
-        me = self.app.me
-        if me is None:
-            return None
-
-        return self.get_member(me.id)
-
-    def get_presence(
-        self, user: snowflakes.SnowflakeishOr[users.PartialUser]
-    ) -> typing.Optional[presences_.MemberPresence]:
-        """Get a cached presence that belongs to the guild by it's user ID or object.
-
-        Parameters
-        ----------
-        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
-            The object or ID of the user to get the cached presence for.
-
-        Returns
-        -------
-        typing.Optional[hikari.presences.MemberPresence]
-            The cached presence object if found, else `builtins.None`.
-        """
-        if not isinstance(self.app, traits.CacheAware):
-            return None
-
-        return self.app.cache.get_presence(self.id, user)
-
     def get_role(self, role: snowflakes.SnowflakeishOr[PartialRole]) -> typing.Optional[Role]:
         """Get a cached role that belongs to the guild by it's ID or object.
 
@@ -1626,23 +1650,3 @@ def get_role(self, role: snowflakes.SnowflakeishOr[PartialRole]) -> typing.Optio
             return None
 
         return self.app.cache.get_role(role)
-
-    def get_voice_state(
-        self, user: snowflakes.SnowflakeishOr[users.PartialUser]
-    ) -> typing.Optional[voices_.VoiceState]:
-        """Get a cached voice state that belongs to the guild by it's user.
-
-        Parameters
-        ----------
-        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
-            The object or ID of the user to get the cached voice state for.
-
-        Returns
-        -------
-        typing.Optional[hikari.voices.VoiceState]
-            The cached voice state object if found, else `builtins.None`.
-        """
-        if not isinstance(self.app, traits.CacheAware):
-            return None
-
-        return self.app.cache.get_voice_state(self.id, user)
diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py
index 7a67133e..a1c7db17 100644
--- a/hikari/impl/bot.py
+++ b/hikari/impl/bot.py
@@ -213,7 +213,6 @@ class BotApp(traits.BotAware):
     __slots__: typing.Sequence[str] = (
         "_cache",
         "_closing_event",
-        "_closed",
         "_entity_factory",
         "_event_manager",
         "_event_factory",
@@ -250,8 +249,7 @@ def __init__(
         self.print_banner(banner, allow_color, force_color)
 
         # Settings and state
-        self._closing_event = asyncio.Event()
-        self._closed = False
+        self._closing_event: typing.Optional[asyncio.Event] = None
         self._is_alive = False
         self._executor = executor
         self._http_settings = http_settings if http_settings is not None else config.HTTPSettings()
@@ -277,6 +275,7 @@ def __init__(
 
         # RESTful API.
         self._rest = rest_impl.RESTClientImpl(
+            cache=self._cache,
             entity_factory=self._entity_factory,
             executor=self._executor,
             http_settings=self._http_settings,
@@ -359,15 +358,14 @@ def _check_if_alive(self) -> None:
 
     async def close(self, force: bool = True) -> None:
         """Kill the application by shutting all components down."""
-        if not self._closing_event.is_set():
+        if self._closing_event and not self._closing_event.is_set():
             _LOGGER.debug("bot requested to shutdown [force:%s]", force)
+            self._closing_event.set()
 
-        self._closing_event.set()
-
-        if self._closed or not force:
+        if not self._closing_event or not force:  # If closing event is None then this is already closing.
             return
 
-        self._closed = True
+        self._closing_event = None
 
         async def handle(name: str, awaitable: typing.Awaitable[typing.Any]) -> None:
             future = asyncio.ensure_future(awaitable)
@@ -420,7 +418,7 @@ async def join(self, until_close: bool = True) -> None:
         self._check_if_alive()
 
         awaitables: typing.List[typing.Awaitable[typing.Any]] = [s.join() for s in self._shards.values()]
-        if until_close:
+        if until_close and self._closing_event:  # If closing event is None then this is already closing.
             awaitables.append(self._closing_event.wait())
 
         await aio.first_completed(*awaitables)
@@ -576,7 +574,7 @@ def run(
         if shard_ids is not None and shard_count is None:
             raise TypeError("'shard_ids' must be passed with 'shard_count'")
 
-        loop = asyncio.get_event_loop()
+        loop = aio.get_or_make_loop()
         signals = ("SIGINT", "SIGTERM")
 
         if asyncio_debug:
@@ -779,7 +777,7 @@ async def start(
             raise errors.GatewayError("Attempted to start more sessions than were allowed in the given time-window")
 
         self._is_alive = True
-        self._closing_event.clear()
+        self._closing_event = asyncio.Event()
         _LOGGER.info(
             "you can start %s session%s before the next window which starts at %s; planning to start %s session%s... ",
             requirements.session_start_limit.remaining,
@@ -843,6 +841,7 @@ async def start(
                         shard_id=candidate_shard_id,
                         shard_count=shard_count,
                         url=requirements.url,
+                        closing_event=self._closing_event,
                     )
                     for candidate_shard_id in window
                     if candidate_shard_id in shard_ids
@@ -863,10 +862,8 @@ def stream(
         self._check_if_alive()
         return self._event_manager.stream(event_type, timeout=timeout, limit=limit)
 
-    def subscribe(
-        self, event_type: typing.Type[typing.Any], callback: event_manager_.CallbackT[typing.Any]
-    ) -> event_manager_.CallbackT[typing.Any]:
-        return self._event_manager.subscribe(event_type, callback)
+    def subscribe(self, event_type: typing.Type[typing.Any], callback: event_manager_.CallbackT[typing.Any]) -> None:
+        self._event_manager.subscribe(event_type, callback)
 
     def unsubscribe(self, event_type: typing.Type[typing.Any], callback: event_manager_.CallbackT[typing.Any]) -> None:
         self._event_manager.unsubscribe(event_type, callback)
@@ -954,6 +951,7 @@ async def _start_one_shard(
         shard_id: int,
         shard_count: int,
         url: str,
+        closing_event: asyncio.Event,
     ) -> shard_impl.GatewayShardImpl:
         new_shard = shard_impl.GatewayShardImpl(
             http_settings=self._http_settings,
@@ -974,7 +972,7 @@ async def _start_one_shard(
         self._shards[shard_id] = new_shard
 
         start = time.monotonic()
-        await aio.first_completed(new_shard.start(), self._closing_event.wait())
+        await aio.first_completed(new_shard.start(), closing_event.wait())
         end = time.monotonic()
 
         if new_shard.is_alive:
@@ -1025,6 +1023,8 @@ async def murder(future: asyncio.Future[typing.Any]) -> None:
 
         _LOGGER.debug("closing event loop")
         loop.close()
+        # Closed loops cannot be re-used so it should also be un-set.
+        asyncio.set_event_loop(None)
 
     @staticmethod
     def _validate_activity(activity: undefined.UndefinedNoneOr[presences.Activity]) -> None:
diff --git a/hikari/impl/buckets.py b/hikari/impl/buckets.py
index 8fc48e5e..9ee16908 100644
--- a/hikari/impl/buckets.py
+++ b/hikari/impl/buckets.py
@@ -353,9 +353,6 @@ class RESTBucketManager:
         greater than this will instead raise an error.
     """
 
-    _POLL_PERIOD: typing.Final[typing.ClassVar[int]] = 20
-    _EXPIRE_PERIOD: typing.Final[typing.ClassVar[int]] = 10
-
     __slots__: typing.Sequence[str] = (
         "routes_to_hashes",
         "real_hashes_to_buckets",
@@ -406,7 +403,7 @@ def __exit__(
     def __del__(self) -> None:
         self.close()
 
-    def start(self, poll_period: float = _POLL_PERIOD, expire_after: float = _EXPIRE_PERIOD) -> None:
+    def start(self, poll_period: float = 20.0, expire_after: float = 10.0) -> None:
         """Start this ratelimiter up.
 
         This spins up internal garbage collection logic in the background to
diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py
index 4fe5ad87..451b98d0 100644
--- a/hikari/impl/cache.py
+++ b/hikari/impl/cache.py
@@ -67,6 +67,7 @@ class CacheImpl(cache.MutableCache):
 
     __slots__: typing.Sequence[str] = (
         "_app",
+        "_dm_channel_entries",
         "_emoji_entries",
         "_guild_channel_entries",
         "_guild_entries",
@@ -81,6 +82,7 @@ class CacheImpl(cache.MutableCache):
         "_settings",
     )
 
+    _dm_channel_entries: collections.ExtendedMutableMapping[snowflakes.Snowflake, snowflakes.Snowflake]
     # For the sake of keeping things clean, the annotations are being kept separate from the assignment here.
     _me: typing.Optional[users.OwnUser]
     _emoji_entries: collections.ExtendedMutableMapping[snowflakes.Snowflake, cache_utility.KnownCustomEmojiData]
@@ -112,6 +114,7 @@ def settings(self) -> config.CacheSettings:
         return self._settings
 
     def _create_cache(self) -> None:
+        self._dm_channel_entries = collections.FreezableDict()
         self._emoji_entries = collections.FreezableDict()
         self._guild_channel_entries = collections.FreezableDict()
         self._guild_entries = collections.FreezableDict()
@@ -139,6 +142,34 @@ def clear(self) -> None:
 
         self._create_cache()
 
+    def clear_dm_channel_ids(self) -> cache.CacheView[snowflakes.Snowflake, snowflakes.Snowflake]:
+        result = self._dm_channel_entries
+        self._dm_channel_entries = collections.FreezableDict()
+        return cache_utility.CacheMappingView(result)
+
+    def delete_dm_channel_id(
+        self, user: snowflakes.SnowflakeishOr[users.PartialUser], /
+    ) -> typing.Optional[snowflakes.Snowflake]:
+        return self._dm_channel_entries.pop(snowflakes.Snowflake(user), None)
+
+    def get_dm_channel_id(
+        self, user: snowflakes.SnowflakeishOr[users.PartialUser], /
+    ) -> typing.Optional[snowflakes.Snowflake]:
+        return self._dm_channel_entries.get(snowflakes.Snowflake(user))
+
+    def get_dm_channel_ids_view(self) -> cache.CacheView[snowflakes.Snowflake, snowflakes.Snowflake]:
+        return cache_utility.CacheMappingView(self._dm_channel_entries.freeze())
+
+    def set_dm_channel_id(
+        self,
+        user: snowflakes.SnowflakeishOr[users.PartialUser],
+        channel: snowflakes.SnowflakeishOr[channels.PartialChannel],
+        /,
+    ) -> None:
+        user = snowflakes.Snowflake(user)
+        if self._is_cache_enabled_for(config.CacheComponents.EMOJIS) and user in self._user_entries:
+            self._dm_channel_entries[user] = snowflakes.Snowflake(channel)
+
     def _build_emoji(
         self,
         emoji_data: cache_utility.KnownCustomEmojiData,
@@ -1192,6 +1223,7 @@ def _garbage_collect_user(
 
         if self._can_remove_user(user) and user.object.id in self._user_entries:
             del self._user_entries[user.object.id]
+            self._dm_channel_entries.pop(user.object.id, None)
 
     def get_user(self, user: snowflakes.SnowflakeishOr[users.PartialUser], /) -> typing.Optional[users.User]:
         user = self._user_entries.get(snowflakes.Snowflake(user))
diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py
index a0cdd76a..3e3767ed 100644
--- a/hikari/impl/event_factory.py
+++ b/hikari/impl/event_factory.py
@@ -65,6 +65,8 @@
 class EventFactoryImpl(event_factory.EventFactory):
     """Implementation for a single-application bot event factory."""
 
+    __slots__: typing.Sequence[str] = ("_app",)
+
     def __init__(self, app: traits.RESTAware) -> None:
         self._app = app
 
@@ -627,9 +629,9 @@ def deserialize_message_reaction_remove_emoji_event(
 
     def deserialize_shard_payload_event(
         self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject, *, name: str
-    ) -> shard_events.ShardPayload:
+    ) -> shard_events.ShardPayloadEvent:
         payload = types.MappingProxyType(payload)
-        return shard_events.ShardPayload(app=self._app, shard=shard, payload=payload, name=name)
+        return shard_events.ShardPayloadEvent(app=self._app, shard=shard, payload=payload, name=name)
 
     def deserialize_ready_event(
         self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
diff --git a/hikari/impl/event_manager_base.py b/hikari/impl/event_manager_base.py
index 33266a7f..f20220a7 100644
--- a/hikari/impl/event_manager_base.py
+++ b/hikari/impl/event_manager_base.py
@@ -97,7 +97,7 @@ def subscribe(
         callback: event_manager.CallbackT[event_manager.EventT_co],
         *,
         _nested: int = 0,
-    ) -> event_manager.CallbackT[event_manager.EventT_co]:
+    ) -> None:
         if not issubclass(event_type, base_events.Event):
             raise TypeError("Cannot subscribe to a non-Event type")
 
@@ -121,8 +121,6 @@ def subscribe(
 
         self._listeners[event_type].append(callback)  # type: ignore[arg-type]
 
-        return callback
-
     def _check_intents(self, event_type: typing.Type[event_manager.EventT_co], nested: int) -> None:
         # Collection of combined bitfield combinations of intents that
         # could be enabled to receive this event.
@@ -268,7 +266,7 @@ async def wait_for(
 
         self._check_intents(event_type, 1)
 
-        future: asyncio.Future[event_manager.EventT_co] = asyncio.get_event_loop().create_future()
+        future: asyncio.Future[event_manager.EventT_co] = asyncio.get_running_loop().create_future()
 
         try:
             waiter_set = self._waiters[event_type]
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index de34f016..94a4857b 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -91,6 +91,7 @@
     from hikari import templates
     from hikari import voices
     from hikari import webhooks
+    from hikari.api import cache as cache_api
     from hikari.api import entity_factory as entity_factory_
 
 _LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.rest")
@@ -110,6 +111,10 @@
 _X_RATELIMIT_REMAINING_HEADER: typing.Final[str] = sys.intern("X-RateLimit-Remaining")
 _X_RATELIMIT_RESET_AFTER_HEADER: typing.Final[str] = sys.intern("X-RateLimit-Reset-After")
 
+# Custom emoji mentions are in the format of <:name:id> for static emoji, or
+# <a:name:id> for animated emoji.
+_CUSTOM_EMOJI_PATTERN: typing.Final[typing.Pattern[str]] = re.compile(r"<a?:([^:]+:\d+)>")
+
 
 class ClientCredentialsStrategy(rest_api.TokenStrategy):
     """Strategy class for handling client credential OAuth2 authorization.
@@ -182,6 +187,10 @@ def scopes(self) -> typing.Sequence[typing.Union[applications.OAuth2Scope, str]]
         """
         return self._scopes
 
+    @property
+    def token_type(self) -> applications.TokenType:
+        return applications.TokenType.BEARER
+
     async def acquire(self, client: rest_api.RESTClient) -> str:
         if self._token and not self._is_expired:
             return self._token
@@ -254,6 +263,9 @@ def proxy_settings(self) -> config.ProxySettings:
         return self._rest().proxy_settings
 
 
+_NONE_OR_UNDEFINED: typing.Final[typing.Set[undefined.UndefinedOr[None]]] = {None, undefined.UNDEFINED}
+
+
 class RESTApp(traits.ExecutorAware):
     """The base for a HTTP-only Discord application.
 
@@ -326,18 +338,76 @@ def http_settings(self) -> config.HTTPSettings:
     def proxy_settings(self) -> config.ProxySettings:
         return self._proxy_settings
 
+    @typing.overload
+    def acquire(self, token: typing.Optional[rest_api.TokenStrategy] = None) -> RESTClientImpl:
+        ...
+
+    @typing.overload
     def acquire(
         self,
-        token: typing.Union[str, rest_api.TokenStrategy, None] = None,
+        token: str,
         token_type: typing.Union[str, applications.TokenType] = applications.TokenType.BEARER,
     ) -> RESTClientImpl:
+        ...
+
+    def acquire(
+        self,
+        token: typing.Union[str, rest_api.TokenStrategy, None] = None,
+        token_type: typing.Union[str, applications.TokenType, None] = None,
+    ) -> RESTClientImpl:
+        """Acquire an instance of this REST client.
+
+        !!! note
+            The returned REST client should be started before it can be used,
+            either by calling `RESTClientImpl.start` or by using it as an
+            asynchronous context manager.
+
+        Examples
+        --------
+        ```py
+        rest_app = RESTApp()
+
+        # Using the returned client as a context manager to implicitly start
+        # and stop it.
+        async with rest_app.acquire("A token", "Bot") as client:
+            user = await client.fetch_my_user()
+        ```
+
+        Parameters
+        ----------
+        token : typing.Union[builtins.str, builtins.None, hikari.api.rest.TokenStrategy]
+            The bot or bearer token. If no token is to be used,
+            this can be undefined.
+        token_type : typing.Union[builtins.str, hikari.applications.TokenType, builtins.None]
+            The type of token in use. This should only be passed when `builtins.str`
+            is passed for `token`, can be `"Bot"` or `"Bearer"` and will be
+            defaulted to `"Bearer"` in this situation.
+
+            This should be left as `builtins.None` when either
+            `hikari.api.rest.TokenStrategy` or `builtins.None` is passed for
+            `token`.
+
+        Returns
+        -------
+        RESTClientImpl
+            An instance of the REST client.
+
+        Raises
+        ------
+        builtins.ValueError
+            If `token_type` is provided when a token strategy is passed for `token`.
+        """
         # Since we essentially mimic a fake App instance, we need to make a circular provider.
         # We can achieve this using a lambda. This allows the entity factory to build models that
         # are also REST-aware
         provider = _RESTProvider(lambda: entity_factory, self._executor, lambda: rest_client)
         entity_factory = entity_factory_impl.EntityFactoryImpl(provider)
 
+        if token_type is None and isinstance(token, str):
+            token_type = applications.TokenType.BEARER
+
         rest_client = RESTClientImpl(
+            cache=None,
             entity_factory=entity_factory,
             executor=self._executor,
             http_settings=self._http_settings,
@@ -376,16 +446,27 @@ class RESTClientImpl(rest_api.RESTClient):
         The bot or bearer token. If no token is to be used,
         this can be undefined.
     token_type : typing.Union[builtins.str, hikari.applications.TokenType, builtins.None]
-        The type of token in use. If no token is used, this can be ignored and
-        left to the default value. This can be `"Bot"` or `"Bearer"`.
+        The type of token in use. This must be passed when a `builtins.str` is
+        passed for `token` but and can be `"Bot"` or `"Bearer"`.
+
+        This should be left as `builtins.None` when either
+        `hikari.api.rest.TokenStrategy` or `builtins.None` is passed for
+        `token`.
     rest_url : builtins.str
         The HTTP API base URL. This can contain format-string specifiers to
         interpolate information such as API version in use.
+
+    Raises
+    ------
+    builtins.ValueError
+        * If `token_type` is provided when a token strategy is passed for `token`.
+        * if `token_type` is left as `builtins.None` when a string is passed for `token`.
     """
 
     __slots__: typing.Sequence[str] = (
         "buckets",
         "global_rate_limit",
+        "_cache",
         "_client_session",
         "_closed_event",
         "_tcp_connector",
@@ -395,6 +476,7 @@ class RESTClientImpl(rest_api.RESTClient):
         "_proxy_settings",
         "_rest_url",
         "_token",
+        "_token_type",
     )
 
     buckets: buckets_.RESTBucketManager
@@ -410,6 +492,7 @@ class _RetryRequest(RuntimeError):
     def __init__(
         self,
         *,
+        cache: typing.Optional[cache_api.MutableCache],
         entity_factory: entity_factory_.EntityFactory,
         executor: typing.Optional[concurrent.futures.Executor],
         http_settings: config.HTTPSettings,
@@ -423,6 +506,7 @@ def __init__(
         # We've been told in DAPI that this is per token.
         self.global_rate_limit = rate_limits.ManualRateLimiter()
 
+        self._cache = cache
         self._client_session: typing.Optional[aiohttp.ClientSession] = None
         self._tcp_connector: typing.Optional[aiohttp.TCPConnector] = None
         self._closed_event = asyncio.Event()
@@ -431,15 +515,21 @@ def __init__(
         self._http_settings = http_settings
         self._proxy_settings = proxy_settings
 
-        self._token: typing.Union[str, rest_api.TokenStrategy, None]
+        self._token: typing.Union[str, rest_api.TokenStrategy, None] = None
+        self._token_type: typing.Optional[str] = None
         if isinstance(token, str):
-            self._token = f"{token_type.title()} {token}" if token_type else token
+            if token_type is None:
+                raise ValueError("Token type required when a str is passed for `token`")
 
-        elif isinstance(token, rest_api.TokenStrategy) and token_type:
-            raise ValueError("Token type should be handled by the token strategy")
+            self._token = f"{token_type.title()} {token}"
+            self._token_type = applications.TokenType(token_type.title())
+
+        elif isinstance(token, rest_api.TokenStrategy):
+            if token_type is not None:
+                raise ValueError("Token type should be handled by the token strategy")
 
-        else:
             self._token = token
+            self._token_type = token.token_type
 
         self._rest_url = rest_url if rest_url is not None else urls.REST_API_URL
 
@@ -451,6 +541,10 @@ def http_settings(self) -> config.HTTPSettings:
     def proxy_settings(self) -> config.ProxySettings:
         return self._proxy_settings
 
+    @property
+    def token_type(self) -> typing.Union[str, applications.TokenType, None]:
+        return self._token_type
+
     @typing.final
     async def close(self) -> None:
         """Close the HTTP client and any open HTTP connections."""
@@ -787,7 +881,12 @@ async def fetch_channel(
         route = routes.GET_CHANNEL.compile(channel=channel)
         response = await self._request(route)
         assert isinstance(response, dict)
-        return self._entity_factory.deserialize_channel(response)
+        result = self._entity_factory.deserialize_channel(response)
+
+        if self._cache and isinstance(result, channels_.DMChannel):
+            self._cache.set_dm_channel_id(result.recipient.id, result.id)
+
+        return result
 
     async def edit_channel(
         self,
@@ -1052,38 +1151,42 @@ async def fetch_message(
         assert isinstance(response, dict)
         return self._entity_factory.deserialize_message(response)
 
-    async def create_message(
+    async def _create_message(
         self,
-        channel: snowflakes.SnowflakeishOr[channels_.TextChannel],
-        content: undefined.UndefinedOr[typing.Any] = undefined.UNDEFINED,
+        route: routes.CompiledRoute,
+        body: data_binding.JSONObjectBuilder,
+        query: typing.Optional[data_binding.StringMapBuilder] = None,
         *,
-        embed: undefined.UndefinedOr[embeds_.Embed] = undefined.UNDEFINED,
-        attachment: undefined.UndefinedOr[files.Resourceish] = undefined.UNDEFINED,
-        attachments: undefined.UndefinedOr[typing.Sequence[files.Resourceish]] = undefined.UNDEFINED,
-        tts: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
-        nonce: undefined.UndefinedOr[str] = undefined.UNDEFINED,
-        reply: undefined.UndefinedOr[snowflakes.SnowflakeishOr[messages_.PartialMessage]] = undefined.UNDEFINED,
-        mentions_everyone: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
-        mentions_reply: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
-        user_mentions: undefined.UndefinedOr[
-            typing.Union[snowflakes.SnowflakeishSequence[users.PartialUser], bool]
-        ] = undefined.UNDEFINED,
-        role_mentions: undefined.UndefinedOr[
-            typing.Union[snowflakes.SnowflakeishSequence[guilds.PartialRole], bool]
-        ] = undefined.UNDEFINED,
+        content: undefined.UndefinedOr[typing.Any],
+        embed: undefined.UndefinedOr[embeds_.Embed],
+        embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]],
+        attachment: undefined.UndefinedOr[files.Resourceish],
+        attachments: undefined.UndefinedOr[typing.Sequence[files.Resourceish]],
+        tts: undefined.UndefinedOr[bool],
+        mentions_everyone: undefined.UndefinedOr[bool],
+        mentions_reply: undefined.UndefinedOr[bool],
+        user_mentions: undefined.UndefinedOr[typing.Union[snowflakes.SnowflakeishSequence[users.PartialUser], bool]],
+        role_mentions: undefined.UndefinedOr[typing.Union[snowflakes.SnowflakeishSequence[guilds.PartialRole], bool]],
     ) -> messages_.Message:
         if not undefined.count(attachment, attachments):
             raise ValueError("You may only specify one of 'attachment' or 'attachments', not both")
 
-        if not isinstance(attachments, typing.Collection) and attachments is not undefined.UNDEFINED:
+        if not undefined.count(embed, embeds):
+            raise ValueError("You may only specify one of 'embed' or 'embeds', not both")
+
+        if attachments is not undefined.UNDEFINED and not isinstance(attachments, typing.Collection):
             raise ValueError(
                 "You passed a non-collection to 'attachments', but this expects a collection. Maybe you meant to "
                 "use 'attachment' (singular) instead?"
             )
 
-        route = routes.POST_CHANNEL_MESSAGES.compile(channel=channel)
+        if embeds is not undefined.UNDEFINED and not isinstance(embeds, typing.Collection):
+            raise TypeError(
+                "You passed a non collection to 'embeds', but this expects a collection. Maybe you meant to "
+                "use 'embed' (singular) instead?"
+            )
 
-        if embed is undefined.UNDEFINED and isinstance(content, embeds_.Embed):
+        if undefined.count(embed, embeds) == 2 and isinstance(content, embeds_.Embed):
             # Syntatic sugar, common mistake to accidentally send an embed
             # as the content, so lets detect this and fix it for the user.
             embed = content
@@ -1099,15 +1202,12 @@ async def create_message(
             attachment = content
             content = undefined.UNDEFINED
 
-        body = data_binding.JSONObjectBuilder()
         body.put(
             "allowed_mentions",
             self._generate_allowed_mentions(mentions_everyone, mentions_reply, user_mentions, role_mentions),
         )
         body.put("content", content, conversion=str)
-        body.put("nonce", nonce)
         body.put("tts", tts)
-        body.put("message_reference", reply, conversion=lambda m: {"message_id": str(int(m))})
 
         final_attachments: typing.List[files.Resource[files.AsyncReader]] = []
         if attachment is not undefined.UNDEFINED:
@@ -1115,10 +1215,19 @@ async def create_message(
         if attachments is not undefined.UNDEFINED:
             final_attachments.extend([files.ensure_resource(a) for a in attachments])
 
+        serialized_embeds: data_binding.JSONArray = []
+
         if embed is not undefined.UNDEFINED:
             embed_payload, embed_attachments = self._entity_factory.serialize_embed(embed)
-            body.put("embed", embed_payload)
             final_attachments.extend(embed_attachments)
+            serialized_embeds.append(embed_payload)
+        elif embeds is not undefined.UNDEFINED:
+            for embed in embeds:
+                embed_payload, embed_attachments = self._entity_factory.serialize_embed(embed)
+                final_attachments.extend(embed_attachments)
+                serialized_embeds.append(embed_payload)
+
+        body.put("embeds", serialized_embeds)
 
         if final_attachments:
             form = data_binding.URLEncodedForm()
@@ -1132,37 +1241,27 @@ async def create_message(
                     mimetype = stream.mimetype or _APPLICATION_OCTET_STREAM
                     form.add_field(f"file{i}", stream, filename=stream.filename, content_type=mimetype)
 
-                response = await self._request(route, form=form)
+                response = await self._request(route, form=form, query=query)
             finally:
                 await stack.aclose()
         else:
-            response = await self._request(route, json=body)
+            response = await self._request(route, json=body, query=query)
 
         assert isinstance(response, dict)
         return self._entity_factory.deserialize_message(response)
 
-    async def crosspost_message(
-        self,
-        channel: snowflakes.SnowflakeishOr[channels_.GuildNewsChannel],
-        message: snowflakes.SnowflakeishOr[messages_.PartialMessage],
-    ) -> messages_.Message:
-        route = routes.POST_CHANNEL_CROSSPOST.compile(channel=channel, message=message)
-
-        response = await self._request(route)
-
-        assert isinstance(response, dict)
-        return self._entity_factory.deserialize_message(response)
-
-    async def edit_message(
+    async def create_message(
         self,
         channel: snowflakes.SnowflakeishOr[channels_.TextChannel],
-        message: snowflakes.SnowflakeishOr[messages_.PartialMessage],
         content: undefined.UndefinedOr[typing.Any] = undefined.UNDEFINED,
         *,
-        embed: undefined.UndefinedNoneOr[embeds_.Embed] = undefined.UNDEFINED,
+        embed: undefined.UndefinedOr[embeds_.Embed] = undefined.UNDEFINED,
+        embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED,
         attachment: undefined.UndefinedOr[files.Resourceish] = undefined.UNDEFINED,
         attachments: undefined.UndefinedOr[typing.Sequence[files.Resourceish]] = undefined.UNDEFINED,
-        replace_attachments: bool = False,
+        tts: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
+        nonce: undefined.UndefinedOr[str] = undefined.UNDEFINED,
+        reply: undefined.UndefinedOr[snowflakes.SnowflakeishOr[messages_.PartialMessage]] = undefined.UNDEFINED,
         mentions_everyone: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
         mentions_reply: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
         user_mentions: undefined.UndefinedOr[
@@ -1171,11 +1270,72 @@ async def edit_message(
         role_mentions: undefined.UndefinedOr[
             typing.Union[snowflakes.SnowflakeishSequence[guilds.PartialRole], bool]
         ] = undefined.UNDEFINED,
-        flags: undefined.UndefinedOr[messages_.MessageFlag] = undefined.UNDEFINED,
     ) -> messages_.Message:
-        route = routes.PATCH_CHANNEL_MESSAGE.compile(channel=channel, message=message)
+        route = routes.POST_CHANNEL_MESSAGES.compile(channel=channel)
         body = data_binding.JSONObjectBuilder()
-        body.put("flags", flags)
+        body.put("nonce", nonce)
+        body.put("message_reference", reply, conversion=lambda m: {"message_id": str(int(m))})
+        return await self._create_message(
+            route,
+            body,
+            content=content,
+            embed=embed,
+            embeds=embeds,
+            attachment=attachment,
+            attachments=attachments,
+            tts=tts,
+            mentions_everyone=mentions_everyone,
+            mentions_reply=mentions_reply,
+            user_mentions=user_mentions,
+            role_mentions=role_mentions,
+        )
+
+    async def crosspost_message(
+        self,
+        channel: snowflakes.SnowflakeishOr[channels_.GuildNewsChannel],
+        message: snowflakes.SnowflakeishOr[messages_.PartialMessage],
+    ) -> messages_.Message:
+        route = routes.POST_CHANNEL_CROSSPOST.compile(channel=channel, message=message)
+
+        response = await self._request(route)
+
+        assert isinstance(response, dict)
+        return self._entity_factory.deserialize_message(response)
+
+    async def _edit_message(  # noqa: C901 - Function too complex
+        self,
+        route: routes.CompiledRoute,
+        body: data_binding.JSONObjectBuilder,
+        *,
+        content: undefined.UndefinedOr[typing.Any],
+        embed: undefined.UndefinedNoneOr[embeds_.Embed],
+        embeds: undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]],
+        attachment: undefined.UndefinedOr[files.Resourceish],
+        attachments: undefined.UndefinedOr[typing.Sequence[files.Resourceish]],
+        replace_attachments: bool,
+        mentions_everyone: undefined.UndefinedOr[bool],
+        mentions_reply: undefined.UndefinedOr[bool],
+        user_mentions: undefined.UndefinedOr[typing.Union[snowflakes.SnowflakeishSequence[users.PartialUser], bool]],
+        role_mentions: undefined.UndefinedOr[typing.Union[snowflakes.SnowflakeishSequence[guilds.PartialRole], bool]],
+    ) -> messages_.Message:
+        if not undefined.count(attachment, attachments):
+            raise ValueError("You may only specify one of 'attachment' or 'attachments', not both")
+
+        if not undefined.count(embed, embeds):
+            raise ValueError("You may only specify one of 'embed' or 'embeds', not both")
+
+        if attachments is not undefined.UNDEFINED and not isinstance(attachments, typing.Collection):
+            raise ValueError(
+                "You passed a non-collection to 'attachments', but this expects a collection. Maybe you meant to "
+                "use 'attachment' (singular) instead?"
+            )
+
+        if embeds not in _NONE_OR_UNDEFINED and not isinstance(embeds, typing.Collection):
+            raise TypeError(
+                "You passed a non collection to 'embeds', but this expects a collection. Maybe you meant to "
+                "use 'embed' (singular) instead?"
+            )
+
         if undefined.count(mentions_everyone, mentions_reply, user_mentions, role_mentions) != 4:
             body.put(
                 "allowed_mentions",
@@ -1205,16 +1365,27 @@ async def edit_message(
         final_attachments: typing.List[files.Resource[files.AsyncReader]] = []
         if attachment is not undefined.UNDEFINED:
             final_attachments.append(files.ensure_resource(attachment))
-        if attachments:
+        if attachments is not undefined.UNDEFINED:
             final_attachments.extend([files.ensure_resource(a) for a in attachments])
 
-        if isinstance(embed, embeds_.Embed):
-            embed_payload, embed_attachments = self._entity_factory.serialize_embed(embed)
-            body.put("embed", embed_payload)
-            final_attachments.extend(embed_attachments)
+        serialized_embeds: data_binding.JSONArray = []
+        update_embeds: bool = False
+        if embed is not undefined.UNDEFINED:
+            update_embeds = True
+            if embed is not None:
+                embed_payload, embed_attachments = self._entity_factory.serialize_embed(embed)
+                serialized_embeds.append(embed_payload)
+                final_attachments.extend(embed_attachments)
+        elif embeds is not undefined.UNDEFINED:
+            update_embeds = True
+            if embeds is not None:
+                for embed in embeds:
+                    embed_payload, embed_attachments = self._entity_factory.serialize_embed(embed)
+                    serialized_embeds.append(embed_payload)
+                    final_attachments.extend(embed_attachments)
 
-        elif embed is None:
-            body.put("embed", None)
+        if update_embeds:
+            body.put("embeds", serialized_embeds)
 
         if replace_attachments:
             body.put("attachments", None)
@@ -1239,6 +1410,45 @@ async def edit_message(
         assert isinstance(response, dict)
         return self._entity_factory.deserialize_message(response)
 
+    async def edit_message(
+        self,
+        channel: snowflakes.SnowflakeishOr[channels_.TextChannel],
+        message: snowflakes.SnowflakeishOr[messages_.PartialMessage],
+        content: undefined.UndefinedOr[typing.Any] = undefined.UNDEFINED,
+        *,
+        embed: undefined.UndefinedNoneOr[embeds_.Embed] = undefined.UNDEFINED,
+        embeds: undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED,
+        attachment: undefined.UndefinedOr[files.Resourceish] = undefined.UNDEFINED,
+        attachments: undefined.UndefinedOr[typing.Sequence[files.Resourceish]] = undefined.UNDEFINED,
+        replace_attachments: bool = False,
+        mentions_everyone: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
+        mentions_reply: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
+        user_mentions: undefined.UndefinedOr[
+            typing.Union[snowflakes.SnowflakeishSequence[users.PartialUser], bool]
+        ] = undefined.UNDEFINED,
+        role_mentions: undefined.UndefinedOr[
+            typing.Union[snowflakes.SnowflakeishSequence[guilds.PartialRole], bool]
+        ] = undefined.UNDEFINED,
+        flags: undefined.UndefinedOr[messages_.MessageFlag] = undefined.UNDEFINED,
+    ) -> messages_.Message:
+        route = routes.PATCH_CHANNEL_MESSAGE.compile(channel=channel, message=message)
+        body = data_binding.JSONObjectBuilder()
+        body.put("flags", flags)
+        return await self._edit_message(
+            route,
+            body,
+            content=content,
+            embed=embed,
+            embeds=embeds,
+            attachment=attachment,
+            attachments=attachments,
+            replace_attachments=replace_attachments,
+            mentions_everyone=mentions_everyone,
+            mentions_reply=mentions_reply,
+            user_mentions=user_mentions,
+            role_mentions=role_mentions,
+        )
+
     async def delete_message(
         self,
         channel: snowflakes.SnowflakeishOr[channels_.TextChannel],
@@ -1300,11 +1510,8 @@ async def delete_messages(
             except Exception as ex:
                 raise errors.BulkDeleteError(deleted, pending) from ex
 
-    # Custom emoji mentions are in the format of <:name:id> for static emoji, or
-    # <a:name:id> for animated emoji.
-    _CUSTOM_EMOJI_PATTERN: typing.Final[typing.ClassVar[typing.Pattern[str]]] = re.compile(r"<a?:([^:]+:\d+)>")
-
-    def _transform_emoji_to_url_format(self, emoji: emojis.Emojiish) -> str:
+    @staticmethod
+    def _transform_emoji_to_url_format(emoji: emojis.Emojiish) -> str:
         # Given an emojiish, check if it is a valid custom emoji mention. If it
         # is, then convert it to the name:id format (remove the wrapping
         # characters), then return it. If the emoji is an emojis.CustomEmoji
@@ -1314,7 +1521,7 @@ def _transform_emoji_to_url_format(self, emoji: emojis.Emojiish) -> str:
         if isinstance(emoji, emojis.CustomEmoji):
             return emoji.url_name
 
-        if isinstance(emoji, str) and (custom_mention_match := self._CUSTOM_EMOJI_PATTERN.match(emoji)) is not None:
+        if isinstance(emoji, str) and (custom_mention_match := _CUSTOM_EMOJI_PATTERN.match(emoji)) is not None:
             return custom_mention_match.group(1)
 
         return str(emoji)
@@ -1519,93 +1726,28 @@ async def execute_webhook(
             typing.Union[snowflakes.SnowflakeishSequence[guilds.PartialRole], bool]
         ] = undefined.UNDEFINED,
     ) -> messages_.Message:
-        if not undefined.count(attachment, attachments):
-            raise ValueError("You may only specify one of 'attachment' or 'attachments', not both")
-
-        if not undefined.count(embed, embeds):
-            raise ValueError("You may only specify one of 'embed' or 'embeds', not both")
-
-        if not isinstance(embeds, typing.Collection) and embeds is not undefined.UNDEFINED:
-            raise TypeError(
-                "You passed a non collection to 'embeds', but this expects a collection. Maybe you meant to "
-                "use 'embed' (singular) instead?"
-            )
-
-        if not isinstance(attachments, typing.Collection) and attachments is not undefined.UNDEFINED:
-            raise TypeError(
-                "You passed a non collection to 'attachments', but this expects a collection. Maybe you meant to "
-                "use 'attachment' (singular) instead?"
-            )
-
-        if undefined.count(embed, embeds) == 2 and isinstance(content, embeds_.Embed):
-            # Syntatic sugar, common mistake to accidentally send an embed
-            # as the content, so lets detect this and fix it for the user.
-            embed = content
-            content = undefined.UNDEFINED
-
-        elif undefined.count(attachment, attachments) == 2 and isinstance(
-            content, (files.Resource, files.RAWISH_TYPES, os.PathLike)
-        ):
-            # Syntatic sugar, common mistake to accidentally send an attachment
-            # as the content, so lets detect this and fix it for the user. This
-            # will still then work with normal implicit embed attachments as
-            # we work this out later.
-            attachment = content
-            content = undefined.UNDEFINED
-
         route = routes.POST_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token)
 
-        final_attachments: typing.List[files.Resource[files.AsyncReader]] = []
-        if attachment is not undefined.UNDEFINED:
-            final_attachments.append(files.ensure_resource(attachment))
-        if attachments is not undefined.UNDEFINED:
-            final_attachments.extend([files.ensure_resource(a) for a in attachments])
-
-        serialized_embeds: data_binding.JSONArray = []
-
-        if embed is not undefined.UNDEFINED:
-            embed_payload, embed_attachments = self._entity_factory.serialize_embed(embed)
-            serialized_embeds.append(embed_payload)
-            final_attachments.extend(embed_attachments)
-        elif embeds is not undefined.UNDEFINED:
-            for embed in embeds:
-                embed_payload, embed_attachments = self._entity_factory.serialize_embed(embed)
-                serialized_embeds.append(embed_payload)
-                final_attachments.extend(embed_attachments)
-
         body = data_binding.JSONObjectBuilder()
-        body.put(
-            "mentions",
-            self._generate_allowed_mentions(mentions_everyone, undefined.UNDEFINED, user_mentions, role_mentions),
-        )
-        body.put("content", content, conversion=str)
-        body.put("embeds", serialized_embeds)
         body.put("username", username)
         body.put("avatar_url", avatar_url)
-        body.put("tts", tts)
         query = data_binding.StringMapBuilder()
         query.put("wait", True)
-
-        if final_attachments:
-            form = data_binding.URLEncodedForm()
-            form.add_field("payload_json", data_binding.dump_json(body), content_type=_APPLICATION_JSON)
-
-            stack = contextlib.AsyncExitStack()
-
-            try:
-                for i, attachment in enumerate(final_attachments):
-                    stream = await stack.enter_async_context(attachment.stream(executor=self._executor))
-                    mimetype = stream.mimetype or _APPLICATION_OCTET_STREAM
-                    form.add_field(f"file{i}", stream, filename=stream.filename, content_type=mimetype)
-
-                response = await self._request(route, query=query, form=form, no_auth=True)
-            finally:
-                await stack.aclose()
-        else:
-            response = await self._request(route, query=query, json=body, no_auth=True)
-
-        assert isinstance(response, dict)
-        return self._entity_factory.deserialize_message(response)
+        return await self._create_message(
+            route,
+            body,
+            query,
+            content=content,
+            embed=embed,
+            embeds=embeds,
+            attachment=attachment,
+            attachments=attachments,
+            tts=tts,
+            mentions_everyone=mentions_everyone,
+            user_mentions=user_mentions,
+            role_mentions=role_mentions,
+            mentions_reply=undefined.UNDEFINED,
+        )
 
     async def fetch_webhook_message(
         self,
@@ -1638,91 +1780,22 @@ async def edit_webhook_message(
             typing.Union[snowflakes.SnowflakeishSequence[guilds.PartialRole], bool]
         ] = undefined.UNDEFINED,
     ) -> messages_.Message:
-        if not undefined.count(embed, embeds):
-            raise ValueError("You may only specify one of 'embed' or 'embeds', not both")
-
-        if not isinstance(embeds, typing.Collection) and embeds is not undefined.UNDEFINED:
-            raise TypeError(
-                "You passed a non collection to 'embeds', but this expects a collection. Maybe you meant to "
-                "use 'embed' (singular) instead?"
-            )
-
-        if undefined.count(embed, embeds) == 2 and isinstance(content, embeds_.Embed):
-            # Syntatic sugar, common mistake to accidentally send an embed
-            # as the content, so lets detect this and fix it for the user.
-            embed = content
-            content = undefined.UNDEFINED
-        elif undefined.count(attachment, attachments) == 2 and isinstance(
-            content, (files.Resource, files.RAWISH_TYPES, os.PathLike)
-        ):
-            # Syntatic sugar, common mistake to accidentally send an attachment
-            # as the content, so lets detect this and fix it for the user. This
-            # will still then work with normal implicit embed attachments as
-            # we work this out later.
-            attachment = content
-            content = undefined.UNDEFINED
-
         route = routes.PATCH_WEBHOOK_MESSAGE.compile(webhook=webhook, token=token, message=message)
         body = data_binding.JSONObjectBuilder()
-        if undefined.count(mentions_everyone, user_mentions, role_mentions) != 3:
-            body.put(
-                "allowed_mentions",
-                self._generate_allowed_mentions(mentions_everyone, undefined.UNDEFINED, user_mentions, role_mentions),
-            )
-
-        if content is not None:
-            body.put("content", content, conversion=str)
-        else:
-            body.put("content", None)
-
-        final_attachments: typing.List[files.Resource[files.AsyncReader]] = []
-        if attachment is not undefined.UNDEFINED:
-            final_attachments.append(files.ensure_resource(attachment))
-        if attachments:
-            final_attachments.extend([files.ensure_resource(a) for a in attachments])
-
-        serialized_embeds: data_binding.JSONArray = []
-        update_embeds: bool = False
-        if embed is not undefined.UNDEFINED:
-            update_embeds = True
-            if embed is not None:
-                embed_payload, embed_attachments = self._entity_factory.serialize_embed(embed)
-                serialized_embeds.append(embed_payload)
-                final_attachments.extend(embed_attachments)
-        elif embeds is not undefined.UNDEFINED:
-            update_embeds = True
-            if embeds is not None:
-                for embed in embeds:
-                    embed_payload, embed_attachments = self._entity_factory.serialize_embed(embed)
-                    serialized_embeds.append(embed_payload)
-                    final_attachments.extend(embed_attachments)
-
-        if update_embeds:
-            body.put("embeds", serialized_embeds)
-
-        if replace_attachments:
-            body.put("attachments", None)
-
-        if final_attachments:
-            form = data_binding.URLEncodedForm()
-            form.add_field("payload_json", data_binding.dump_json(body), content_type=_APPLICATION_JSON)
-
-            stack = contextlib.AsyncExitStack()
-
-            try:
-                for i, attachment in enumerate(final_attachments):
-                    stream = await stack.enter_async_context(attachment.stream(executor=self._executor))
-                    mimetype = stream.mimetype or _APPLICATION_OCTET_STREAM
-                    form.add_field(f"file{i}", stream, filename=stream.filename, content_type=mimetype)
-
-                response = await self._request(route, form=form, no_auth=True)
-            finally:
-                await stack.aclose()
-        else:
-            response = await self._request(route, json=body, no_auth=True)
-
-        assert isinstance(response, dict)
-        return self._entity_factory.deserialize_message(response)
+        return await self._edit_message(
+            route,
+            body,
+            content=content,
+            embed=embed,
+            embeds=embeds,
+            attachment=attachment,
+            attachments=attachments,
+            replace_attachments=replace_attachments,
+            mentions_everyone=mentions_everyone,
+            mentions_reply=undefined.UNDEFINED,
+            user_mentions=user_mentions,
+            role_mentions=role_mentions,
+        )
 
     async def delete_webhook_message(
         self,
@@ -1826,7 +1899,12 @@ async def create_dm_channel(self, user: snowflakes.SnowflakeishOr[users.PartialU
         body.put_snowflake("recipient_id", user)
         response = await self._request(route, json=body)
         assert isinstance(response, dict)
-        return self._entity_factory.deserialize_dm(response)
+        channel = self._entity_factory.deserialize_dm(response)
+
+        if self._cache:
+            self._cache.set_dm_channel_id(user, channel.id)
+
+        return channel
 
     async def fetch_application(self) -> applications.Application:
         route = routes.GET_MY_APPLICATION.compile()
diff --git a/hikari/impl/shard.py b/hikari/impl/shard.py
index ba73e7da..ae314152 100644
--- a/hikari/impl/shard.py
+++ b/hikari/impl/shard.py
@@ -28,7 +28,6 @@
 
 import asyncio
 import contextlib
-import json
 import logging
 import platform
 import sys
@@ -156,7 +155,7 @@ async def send_close(self, *, code: int = 1000, message: bytes = b"") -> bool:
     async def receive_json(
         self,
         *,
-        loads: aiohttp.typedefs.JSONDecoder = json.loads,
+        loads: aiohttp.typedefs.JSONDecoder = data_binding.load_json,
         timeout: typing.Optional[float] = None,
     ) -> typing.Any:
         pl = await self._receive_and_check(timeout)
@@ -170,7 +169,7 @@ async def send_json(
         data: data_binding.JSONObject,
         compress: typing.Optional[int] = None,
         *,
-        dumps: aiohttp.typedefs.JSONEncoder = json.dumps,
+        dumps: aiohttp.typedefs.JSONEncoder = data_binding.dump_json,
     ) -> None:
         pl = dumps(data)
         if self.logger.isEnabledFor(ux.TRACE):
@@ -534,7 +533,7 @@ async def _send_json(
         data: data_binding.JSONObject,
         compress: typing.Optional[int] = None,
         *,
-        dumps: aiohttp.typedefs.JSONEncoder = json.dumps,
+        dumps: aiohttp.typedefs.JSONEncoder = data_binding.dump_json,
     ) -> None:
         await self._total_rate_limit.acquire()
 
diff --git a/hikari/intents.py b/hikari/intents.py
index e593154f..978ccc26 100644
--- a/hikari/intents.py
+++ b/hikari/intents.py
@@ -370,4 +370,4 @@ def is_privileged(self) -> bool:
         this intent on the Discord Developer Portal before you can utilise it
         in your application.
         """
-        return bool(self & (self.GUILD_MEMBERS | self.GUILD_PRESENCES))
+        return bool(self & self.ALL_PRIVILEGED)
diff --git a/hikari/internal/aio.py b/hikari/internal/aio.py
index 33a39df0..1a5f552f 100644
--- a/hikari/internal/aio.py
+++ b/hikari/internal/aio.py
@@ -26,6 +26,7 @@
 
 __all__: typing.List[str] = [
     "completed_future",
+    "get_or_make_loop",
     "is_async_iterator",
     "is_async_iterable",
     "first_completed",
@@ -55,8 +56,13 @@ def completed_future(result: typing.Optional[T_inv] = None, /) -> asyncio.Future
     -------
     asyncio.Future[T]
         The completed future.
+
+    Raises
+    ------
+    RuntimeError
+        When called in an environment with no running event loop.
     """
-    future = asyncio.get_event_loop().create_future()
+    future = asyncio.get_running_loop().create_future()
     future.set_result(result)
     # MyPy pretends this type hint is valid when it is not. Probably should be
     # in the standard lib but whatever.
@@ -185,3 +191,27 @@ async def all_of(
             await gatherer  # pragma: no cover
         except asyncio.CancelledError:
             pass
+
+
+def get_or_make_loop() -> asyncio.AbstractEventLoop:
+    """Get the current usable event loop or create a new one.
+
+    Returns
+    -------
+    asyncio.AbstractEventLoop
+    """
+    # get_event_loop will error under oddly specific cases such as if set_event_loop has been called before even
+    # if it was just called with None or if it's called on a thread which isn't the main Thread.
+    try:
+        loop = asyncio.get_event_loop()
+
+        # Closed loops cannot be re-used.
+        if not loop.is_closed():
+            return loop
+
+    except RuntimeError:
+        pass
+
+    loop = asyncio.new_event_loop()
+    asyncio.set_event_loop(loop)
+    return loop
diff --git a/hikari/internal/cache.py b/hikari/internal/cache.py
index 7e6b0156..fd5f4ea3 100644
--- a/hikari/internal/cache.py
+++ b/hikari/internal/cache.py
@@ -983,7 +983,7 @@ def copy_guild_channel(channel: ChannelT) -> ChannelT:
 
 
 class Cache3DMappingView(CacheMappingView[snowflakes.Snowflake, cache.CacheView[KeyT, ValueT]]):
-    """A special case of the Mapping View which avoids copying the already immutable views contained within it."""
+    """A special case of the Mapping View which avoids copying the immutable values contained within it."""
 
     __slots__: typing.Sequence[str] = ()
 
diff --git a/hikari/internal/collections.py b/hikari/internal/collections.py
index 7cde3b5a..6c784da6 100644
--- a/hikari/internal/collections.py
+++ b/hikari/internal/collections.py
@@ -106,7 +106,7 @@ def freeze(self) -> typing.MutableMapping[KeyT, ValueT]:
 class FreezableDict(ExtendedMutableMapping[KeyT, ValueT]):
     """A mapping that wraps a dict, but can also be frozen."""
 
-    __slots__ = ("_data",)
+    __slots__: typing.Sequence[str] = ("_data",)
 
     def __init__(self, source: typing.Optional[typing.Dict[KeyT, ValueT]] = None, /) -> None:
         self._data = source or {}
@@ -138,7 +138,7 @@ def __setitem__(self, key: KeyT, value: ValueT) -> None:
 
 
 class _FrozenDict(typing.MutableMapping[KeyT, ValueT]):
-    __slots__ = ("_source",)
+    __slots__: typing.Sequence[str] = ("_source",)
 
     def __init__(self, source: typing.Dict[KeyT, typing.Tuple[float, ValueT]], /) -> None:
         self._source = source
@@ -182,7 +182,7 @@ class TimedCacheMap(ExtendedMutableMapping[KeyT, ValueT]):
         This will always be called after the entry has been removed.
     """
 
-    __slots__ = ("_data", "_expiry", "_on_expire")
+    __slots__: typing.Sequence[str] = ("_data", "_expiry", "_on_expire")
 
     def __init__(
         self,
@@ -269,7 +269,7 @@ class LimitedCapacityCacheMap(ExtendedMutableMapping[KeyT, ValueT]):
         This will always be called after the entry has been removed.
     """
 
-    __slots__ = ("_data", "_limit", "_on_expire")
+    __slots__: typing.Sequence[str] = ("_data", "_limit", "_on_expire")
 
     def __init__(
         self,
diff --git a/hikari/internal/time.py b/hikari/internal/time.py
index fdcb2578..20e85c60 100644
--- a/hikari/internal/time.py
+++ b/hikari/internal/time.py
@@ -68,7 +68,7 @@
 
 # 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:
+def slow_iso8601_datetime_string_to_datetime(datetime_str: str) -> datetime.datetime:
     """Parse an ISO-8601-like datestring into a datetime.
 
     Parameters
@@ -87,7 +87,7 @@ def iso8601_datetime_string_to_datetime(datetime_str: str) -> datetime.datetime:
     return datetime.datetime.fromisoformat(datetime_str)
 
 
-try:  # pragma: no cover
+try:
     # CISO8601 is around 600x faster than modules like dateutil, which is
     # going to be noticable on big bots where you are parsing hundreds of
     # thousands of "joined_at" fields on users on startup.
@@ -98,9 +98,14 @@ def iso8601_datetime_string_to_datetime(datetime_str: str) -> datetime.datetime:
     # Discord appears to actually use RFC-3339, which isn't a true ISO-8601 implementation,
     # but somewhat of a subset with some edge cases.
     # See https://tools.ietf.org/html/rfc3339#section-5.6
-    iso8601_datetime_string_to_datetime = ciso8601.parse_rfc3339  # noqa: F811 - Redefined function
+    fast_iso8601_datetime_string_to_datetime = ciso8601.parse_rfc3339
+
 except ImportError:
-    pass
+    fast_iso8601_datetime_string_to_datetime = None
+
+iso8601_datetime_string_to_datetime = (
+    fast_iso8601_datetime_string_to_datetime or slow_iso8601_datetime_string_to_datetime
+)
 
 
 def discord_epoch_to_datetime(epoch: int, /) -> datetime.datetime:
diff --git a/hikari/messages.py b/hikari/messages.py
index 9b8b680f..8988452e 100644
--- a/hikari/messages.py
+++ b/hikari/messages.py
@@ -542,8 +542,8 @@ class PartialMessage(snowflakes.Unique):
     If the message is not in a guild, this will be `builtins.None`.
 
     This will also be `builtins.None` in some cases such as when Discord updates
-    a message with an embed URL preview or in messages fetched from the
-    REST API.
+    a message with an embed URL preview, in messages fetched from the
+    REST API or messages sent by discord.
     """
 
     content: undefined.UndefinedNoneOr[str] = attr.field(hash=False, eq=False, repr=False)
@@ -702,6 +702,7 @@ async def edit(
         content: undefined.UndefinedOr[typing.Any] = undefined.UNDEFINED,
         *,
         embed: undefined.UndefinedNoneOr[embeds_.Embed] = undefined.UNDEFINED,
+        embeds: undefined.UndefinedNoneOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED,
         attachment: undefined.UndefinedOr[files.Resourceish] = undefined.UNDEFINED,
         attachments: undefined.UndefinedOr[typing.Sequence[files.Resourceish]] = undefined.UNDEFINED,
         replace_attachments: bool = False,
@@ -720,33 +721,33 @@ async def edit(
         Parameters
         ----------
         content : hikari.undefined.UndefinedOr[typing.Any]
-            The message content to update with. If
+            If provided, the message content to update with. If
             `hikari.undefined.UNDEFINED`, then the content will not
             be changed. If `builtins.None`, then the content will be removed.
 
             Any other value will be cast to a `builtins.str` before sending.
 
-            If this is a `hikari.embeds.Embed` and neither the
-            `embed` or `embeds` kwargs are provided or if this is a
-            `hikari.files.Resourceish` and neither the `attachment` or
-            `attachments` kwargs are provided, the values will be overwritten.
-            This allows for simpler syntax when sending an embed or an
-            attachment alone.
+            If this is a `hikari.embeds.Embed` and neither the `embed` or
+            `embeds` kwargs are provided or if this is a
+            `hikari.files.Resourceish` and neither the
+            `attachment` or `attachments` kwargs are provided, the values will
+            be overwritten. This allows for simpler syntax when sending an
+            embed or an attachment alone.
 
         Other Parameters
         ----------------
         embed : hikari.undefined.UndefinedNoneOr[hikari.embeds.Embed]
             If provided, the embed to set on the message. If
-            `hikari.undefined.UNDEFINED`, the previous embed, if
-            present, is not changed. If this is `builtins.None`, then the embed
-            is removed, if present. Otherwise, the new embed that was provided
-            will be used as the replacement.
-        attachment : hikari.undefined.UndefinedOr[hikari.files.Resourceish]
-            If provided, the attachment to set on the message. If
-            `hikari.undefined.UNDEFINED`, the previous attachment, if
-            present, is not changed. If this is `builtins.None`, then the
-            attachment is removed, if present. Otherwise, the new attachment
-            that was provided will be attached.
+            `hikari.undefined.UNDEFINED`, the previous embed(s) are not changed.
+            If this is `builtins.None` then any present embeds are removed.
+            Otherwise, the new embed that was provided will be used as the
+            replacement.
+        embeds : hikari.undefined.UndefinedNoneOr[typing.Sequence[hikari.embeds.Embed]]
+            If provided, the embeds to set on the message. If
+            `hikari.undefined.UNDEFINED`, the previous embed(s) are not changed.
+            If this is `builtins.None` then any present embeds are removed.
+            Otherwise, the new embeds that were provided will be used as the
+            replacement.
         attachments : hikari.undefined.UndefinedOr[typing.Sequence[hikari.files.Resourceish]]
             If provided, the attachments to set on the message. If
             `hikari.undefined.UNDEFINED`, the previous attachments, if
@@ -836,9 +837,7 @@ async def edit(
             This may be raised in several discrete situations, such as messages
             being empty with no embeds; messages with more than 2000 characters
             in them, embeds that exceed one of the many embed
-            limits; invalid image URLs in embeds; users in `user_mentions` not
-            being mentioned in the message content; roles in `role_mentions` not
-            being mentioned in the message content.
+            limits; invalid image URLs in embeds.
         hikari.errors.UnauthorizedError
             If you are unauthorized to make the request (invalid/missing token).
         hikari.errors.ForbiddenError
@@ -856,6 +855,7 @@ async def edit(
             channel=self.channel_id,
             content=content,
             embed=embed,
+            embeds=embeds,
             attachment=attachment,
             attachments=attachments,
             replace_attachments=replace_attachments,
@@ -871,6 +871,7 @@ async def respond(
         content: undefined.UndefinedOr[typing.Any] = undefined.UNDEFINED,
         *,
         embed: undefined.UndefinedOr[embeds_.Embed] = undefined.UNDEFINED,
+        embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED,
         attachment: undefined.UndefinedOr[files.Resourceish] = undefined.UNDEFINED,
         attachments: undefined.UndefinedOr[typing.Sequence[files.Resourceish]] = undefined.UNDEFINED,
         nonce: undefined.UndefinedOr[str] = undefined.UNDEFINED,
@@ -897,9 +898,9 @@ async def respond(
             in the content. Any other value here will be cast to a
             `builtins.str`.
 
-            If this is a `hikari.embeds.Embed` and no `embed` kwarg is
-            provided, then this will instead update the embed. This allows for
-            simpler syntax when sending an embed alone.
+            If this is a `hikari.embeds.Embed` and no `embed` nor `embeds` kwarg
+            is provided, then this will instead update the embed. This allows
+            for simpler syntax when sending an embed alone.
 
             Likewise, if this is a `hikari.files.Resource`, then the
             content is instead treated as an attachment if no `attachment` and
@@ -909,6 +910,8 @@ async def respond(
         ----------------
         embed : hikari.undefined.UndefinedOr[hikari.embeds.Embed]
             If provided, the message embed.
+        embeds : hikari.undefined.UndefinedOr[typing.Sequence[hikari.embeds.Embed]]
+            If provided, the message embeds.
         attachment : hikari.undefined.UndefinedOr[hikari.files.Resourceish],
             If provided, the message attachment. This can be a resource,
             or string of a path on your computer or a URL.
@@ -984,10 +987,8 @@ async def respond(
             being empty with no attachments or embeds; messages with more than
             2000 characters in them, embeds that exceed one of the many embed
             limits; too many attachments; attachments that are too large;
-            invalid image URLs in embeds; users in `user_mentions` not being
-            mentioned in the message content; roles in `role_mentions` not
-            being mentioned in the message content; `reply` not found
-            or not in the same channel.
+            invalid image URLs in embeds; `reply` not found or not in the same
+            channel.
         hikari.errors.UnauthorizedError
             If you are unauthorized to make the request (invalid/missing token).
         hikari.errors.ForbiddenError
@@ -1001,18 +1002,18 @@ async def respond(
             `role_mentions` or `user_mentions`.
         builtins.TypeError
             If both `attachment` and `attachments` are specified.
-
-        !!! warning
-            You are expected to make a connection to the gateway and identify
-            once before being able to use this endpoint for a bot.
         """  # noqa: E501 - Line too long
         if reply is True:
             reply = self
 
+        elif reply is False:
+            reply = undefined.UNDEFINED
+
         return await self.app.rest.create_message(
             channel=self.channel_id,
             content=content,
             embed=embed,
+            embeds=embeds,
             attachment=attachment,
             attachments=attachments,
             nonce=nonce,
diff --git a/hikari/permissions.py b/hikari/permissions.py
index c9470908..22079c29 100644
--- a/hikari/permissions.py
+++ b/hikari/permissions.py
@@ -26,6 +26,8 @@
 
 __all__: typing.List[str] = ["Permissions"]
 
+import functools
+import operator
 import typing
 
 from hikari.internal import enums
@@ -94,19 +96,49 @@ class Permissions(enums.Flag):
     """Allows creation of instant invites."""
 
     KICK_MEMBERS = 1 << 1
-    """Allows kicking members"""
+    """Allows kicking members.
+
+    !!! note
+        In guilds with server-wide 2FA enabled this permission can only be used
+        by users who have two-factor authentication enabled on their account
+        (or their owner's account in the case of bot users) and the guild owner.
+    """
 
     BAN_MEMBERS = 1 << 2
-    """Allows banning members."""
+    """Allows banning members.
+
+    !!! note
+        In guilds with server-wide 2FA enabled this permission can only be used
+        by users who have two-factor authentication enabled on their account
+        (or their owner's account in the case of bot users) and the guild owner.
+    """
 
     ADMINISTRATOR = 1 << 3
-    """Allows all permissions and bypasses channel permission overwrites."""
+    """Allows all permissions and bypasses channel permission overwrites.
+
+    !!! note
+        In guilds with server-wide 2FA enabled this permission can only be used
+        by users who have two-factor authentication enabled on their account
+        (or their owner's account in the case of bot users) and the guild owner.
+    """
 
     MANAGE_CHANNELS = 1 << 4
-    """Allows management and editing of channels."""
+    """Allows management and editing of channels.
+
+    !!! note
+        In guilds with server-wide 2FA enabled this permission can only be used
+        by users who have two-factor authentication enabled on their account
+        (or their owner's account in the case of bot users) and the guild owner.
+    """
 
     MANAGE_GUILD = 1 << 5
-    """Allows management and editing of the guild."""
+    """Allows management and editing of the guild.
+
+    !!! note
+        In guilds with server-wide 2FA enabled this permission can only be used
+        by users who have two-factor authentication enabled on their account
+        (or their owner's account in the case of bot users) and the guild owner.
+    """
 
     ADD_REACTIONS = 1 << 6
     """Allows for the addition of reactions to messages."""
@@ -130,7 +162,13 @@ class Permissions(enums.Flag):
     """Allows for sending of `/tts` messages."""
 
     MANAGE_MESSAGES = 1 << 13
-    """Allows for deletion of other users messages."""
+    """Allows for deletion of other users messages.
+
+    !!! note
+        In guilds with server-wide 2FA enabled this permission can only be used
+        by users who have two-factor authentication enabled on their account
+        (or their owner's account in the case of bot users) and the guild owner.
+    """
 
     EMBED_LINKS = 1 << 14
     """Links sent by users with this permission will be auto-embedded."""
@@ -179,13 +217,31 @@ class Permissions(enums.Flag):
     """Allows for modification of other users nicknames."""
 
     MANAGE_ROLES = 1 << 28
-    """Allows management and editing of roles."""
+    """Allows management and editing of roles.
+
+    !!! note
+        In guilds with server-wide 2FA enabled this permission can only be used
+        by users who have two-factor authentication enabled on their account
+        (or their owner's account in the case of bot users) and the guild owner.
+    """
 
     MANAGE_WEBHOOKS = 1 << 29
-    """Allows management and editing of webhooks."""
+    """Allows management and editing of webhooks.
+
+    !!! note
+        In guilds with server-wide 2FA enabled this permission can only be used
+        by users who have two-factor authentication enabled on their account
+        (or their owner's account in the case of bot users) and the guild owner.
+    """
 
     MANAGE_EMOJIS = 1 << 30
-    """Allows management and editing of emojis."""
+    """Allows management and editing of emojis.
+
+    !!! note
+        In guilds with server-wide 2FA enabled this permission can only be used
+        by users who have two-factor authentication enabled on their account
+        (or their owner's account in the case of bot users) and the guild owner.
+    """
 
     USE_APPLICATION_COMMANDS = 1 << 31
     """Allows for using the application commands of guild integrations within a text channel."""
@@ -198,3 +254,29 @@ class Permissions(enums.Flag):
         development" by Discord meaning that "it may be changed or removed"
         without warning.
     """
+
+    MANAGE_THREADS = 1 << 34
+    """Allows for deleting and archiving threads, and viewing all private threads.
+
+     !!! note
+        In guilds with server-wide 2FA enabled this permission can only be used
+        by users who have two-factor authentication enabled on their account
+        (or their owner's account in the case of bot users) and the guild owner.
+    """
+
+    USE_PUBLIC_THREADS = 1 << 35
+    """Allows for creating and participating in threads."""
+
+    USE_PRIVATE_THREADS = 1 << 36
+    """Allows for creating and participating in private threads."""
+
+    @classmethod
+    def all_permissions(cls) -> Permissions:
+        """Get an instance of `Permissions` with all the known permissions.
+
+        Returns
+        -------
+        Permissions
+            A permissions instance with all the known permissions.
+        """
+        return functools.reduce(operator.ior, Permissions)
diff --git a/hikari/snowflakes.py b/hikari/snowflakes.py
index 19d90c1b..e26bc928 100644
--- a/hikari/snowflakes.py
+++ b/hikari/snowflakes.py
@@ -124,11 +124,6 @@ def id(self) -> Snowflake:
             The snowflake ID of this object.
         """
 
-    # TODO: make immutable interface, as this is a major risk to consistent hash codes.
-    @id.setter
-    def id(self, value: Snowflake) -> None:
-        """Set the ID on this entity."""
-
     @property
     def created_at(self) -> datetime.datetime:
         """When the object was created."""
diff --git a/hikari/users.py b/hikari/users.py
index cb519579..2b7f24fe 100644
--- a/hikari/users.py
+++ b/hikari/users.py
@@ -32,6 +32,7 @@
 import attr
 
 from hikari import snowflakes
+from hikari import traits
 from hikari import undefined
 from hikari import urls
 from hikari.internal import attr_extensions
@@ -40,11 +41,10 @@
 
 if typing.TYPE_CHECKING:
     from hikari import channels
-    from hikari import embeds
+    from hikari import embeds as embeds_
     from hikari import files
     from hikari import guilds
     from hikari import messages
-    from hikari import traits
 
 
 @typing.final
@@ -180,7 +180,6 @@ def mention(self) -> str:
             The mention string to use.
         """
 
-    @abc.abstractmethod
     async def fetch_dm_channel(self) -> channels.DMChannel:
         """Fetch the DM channel for this user.
 
@@ -209,6 +208,7 @@ async def fetch_dm_channel(self) -> channels.DMChannel:
         hikari.errors.InternalServerError
             If an internal error occurs on Discord while handling the request.
         """
+        return await self.app.rest.create_dm_channel(self.id)
 
     async def fetch_self(self) -> User:
         """Get this user's up-to-date object by performing an API call.
@@ -242,7 +242,8 @@ async def send(
         self,
         content: undefined.UndefinedOr[typing.Any] = undefined.UNDEFINED,
         *,
-        embed: undefined.UndefinedOr[embeds.Embed] = undefined.UNDEFINED,
+        embed: undefined.UndefinedOr[embeds_.Embed] = undefined.UNDEFINED,
+        embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED,
         attachment: undefined.UndefinedOr[files.Resourceish] = undefined.UNDEFINED,
         attachments: undefined.UndefinedOr[typing.Sequence[files.Resourceish]] = undefined.UNDEFINED,
         nonce: undefined.UndefinedOr[str] = undefined.UNDEFINED,
@@ -267,18 +268,21 @@ async def send(
             in the content. Any other value here will be cast to a
             `builtins.str`.
 
-            If this is a `hikari.embeds.Embed` and no `embed` kwarg is
-            provided, then this will instead update the embed. This allows for
-            simpler syntax when sending an embed alone.
+            If this is a `hikari.embeds.Embed` and no `embed` nor `embeds` kwarg
+            is provided, then this will instead update the embed. This allows
+            for simpler syntax when sending an embed alone.
 
             Likewise, if this is a `hikari.files.Resource`, then the
             content is instead treated as an attachment if no `attachment` and
             no `attachments` kwargs are provided.
 
+
         Other Parameters
         ----------------
         embed : hikari.undefined.UndefinedOr[hikari.embeds.Embed]
             If provided, the message embed.
+        embeds : hikari.undefined.UndefinedOr[typing.Sequence[hikari.embeds.Embed]]
+            If provided, the message embeds.
         attachment : hikari.undefined.UndefinedOr[hikari.files.Resourceish],
             If provided, the message attachment. This can be a resource,
             or string of a path on your computer or a URL.
@@ -365,10 +369,8 @@ async def send(
             being empty with no attachments or embeds; messages with more than
             2000 characters in them, embeds that exceed one of the many embed
             limits; too many attachments; attachments that are too large;
-            invalid image URLs in embeds; users in `user_mentions` not being
-            mentioned in the message content; roles in `role_mentions` not
-            being mentioned in the message content; `reply` not found
-            or not in the same channel.
+            invalid image URLs in embeds; `reply` not found or not in the same
+            channel.
         hikari.errors.UnauthorizedError
             If you are unauthorized to make the request (invalid/missing token).
         hikari.errors.ForbiddenError
@@ -389,17 +391,19 @@ async def send(
             nature, and will trigger this exception if they occur.
         hikari.errors.InternalServerError
             If an internal error occurs on Discord while handling the request.
-
-        !!! warning
-            You are expected to make a connection to the gateway and identify
-            once before being able to use this endpoint for a bot.
         """  # noqa: E501 - Line too long
-        channel = await self.fetch_dm_channel()
+        channel_id = None
+        if isinstance(self.app, traits.CacheAware):
+            channel_id = self.app.cache.get_dm_channel_id(self.id)
+
+        if channel_id is None:
+            channel_id = (await self.fetch_dm_channel()).id
 
         return await self.app.rest.create_message(
-            channel=channel.id,
+            channel=channel_id,
             content=content,
             embed=embed,
+            embeds=embeds,
             attachment=attachment,
             attachments=attachments,
             nonce=nonce,
@@ -577,16 +581,6 @@ class PartialUserImpl(PartialUser):
     flags: undefined.UndefinedOr[UserFlag] = attr.field(eq=False, hash=False)
     """Public flags for this user."""
 
-    _dm_channel: typing.Optional[channels.DMChannel] = attr.field(
-        eq=False, hash=False, init=False, repr=False, default=None
-    )
-
-    async def fetch_dm_channel(self) -> channels.DMChannel:
-        if self._dm_channel is None:
-            self._dm_channel = await self.app.rest.create_dm_channel(self.id)
-
-        return self._dm_channel
-
     @property
     def mention(self) -> str:
         """Return a raw mention string for the given user.
@@ -704,7 +698,8 @@ async def send(
         self,
         content: undefined.UndefinedOr[typing.Any] = undefined.UNDEFINED,
         *,
-        embed: undefined.UndefinedOr[embeds.Embed] = undefined.UNDEFINED,
+        embed: undefined.UndefinedOr[embeds_.Embed] = undefined.UNDEFINED,
+        embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED,
         attachment: undefined.UndefinedOr[files.Resourceish] = undefined.UNDEFINED,
         attachments: undefined.UndefinedOr[typing.Sequence[files.Resourceish]] = undefined.UNDEFINED,
         nonce: undefined.UndefinedOr[str] = undefined.UNDEFINED,
diff --git a/hikari/webhooks.py b/hikari/webhooks.py
index f9c2eb6a..7197fb72 100644
--- a/hikari/webhooks.py
+++ b/hikari/webhooks.py
@@ -351,16 +351,16 @@ async def edit_message(
         ----------------
         embed : hikari.undefined.UndefinedNoneOr[hikari.embeds.Embed]
             If provided, the embed to set on the message. If
-            `hikari.undefined.UNDEFINED`, the previous embed, if
-            present, is not changed. If this is `builtins.None`, then the embed
-            is removed, if present. Otherwise, the new embed that was provided
-            will be used as the replacement.
-        embeds : hikari.undefined.UndefinedNoneOr[hikari.embeds.Embed]
+            `hikari.undefined.UNDEFINED`, the previous embed(s) are not changed.
+            If this is `builtins.None` then any present embeds are removed.
+            Otherwise, the new embed that was provided will be used as the
+            replacement.
+        embeds : hikari.undefined.UndefinedNoneOr[typing.Sequence[hikari.embeds.Embed]]
             If provided, the embeds to set on the message. If
-            `hikari.undefined.UNDEFINED`, the previous embeds if
-            present are not changed. If this is `builtins.None`, then the embeds
-            are removed ,if present. Otherwise, the new embeds that were provided
-            will be used as the replacement.
+            `hikari.undefined.UNDEFINED`, the previous embed(s) are not changed.
+            If this is `builtins.None` then any present embeds are removed.
+            Otherwise, the new embeds that were provided will be used as the
+            replacement.
         attachment : hikari.undefined.UndefinedOr[hikari.files.Resourceish]
             If provided, the attachment to set on the message. If
             `hikari.undefined.UNDEFINED`, the previous attachment, if
@@ -440,10 +440,7 @@ async def edit_message(
             This may be raised in several discrete situations, such as messages
             being empty with no attachments or embeds; messages with more than
             2000 characters in them, embeds that exceed one of the many embed
-            limits; too many attachments; attachments that are too large;
-            invalid image URLs in embeds; users in `user_mentions` not being
-            mentioned in the message content; roles in `role_mentions` not
-            being mentioned in the message content.
+            limits; too many attachments; attachments that are too large.
         hikari.errors.UnauthorizedError
             If you are unauthorized to make the request (invalid/missing token).
         hikari.errors.NotFoundError
diff --git a/pipelines/mypy.nox.py b/pipelines/mypy.nox.py
index dcbdebc2..2554662c 100644
--- a/pipelines/mypy.nox.py
+++ b/pipelines/mypy.nox.py
@@ -46,3 +46,15 @@ def generate_stubs(session: nox.Session) -> None:
 
 def _generate_stubs(session: nox.Session) -> None:
     session.run("stubgen", *STUBGEN_GENERATE, "-o", ".", "--include-private", "--no-import")
+
+    for path in STUBGEN_GENERATE:
+        # We add "i" because we turn ".py" into ".pyi"
+        stub_path = path + "i"
+
+        with open(stub_path, "r") as fp:
+            content = fp.read()
+
+        with open(stub_path, "w") as fp:
+            fp.write(
+                "# This file was generated by `nox -s generate-stubs`. Do not manually edit this file!\n\n" + content
+            )


Test patch:
diff --git a/tests/hikari/__init__.py b/tests/hikari/__init__.py
index 9439d83f..e9272020 100644
--- a/tests/hikari/__init__.py
+++ b/tests/hikari/__init__.py
@@ -21,6 +21,7 @@
 # SOFTWARE.
 import asyncio
 import sys
+import typing
 
 import pytest
 
@@ -28,8 +29,10 @@
 
 
 class TestingPolicy(asyncio.DefaultEventLoopPolicy):
-    def set_event_loop(self, loop) -> None:
-        loop.set_debug(True)
+    def set_event_loop(self, loop: typing.Optional[asyncio.AbstractEventLoop]) -> None:
+        if loop:
+            loop.set_debug(True)
+
         super().set_event_loop(loop)
 
 
diff --git a/tests/hikari/events/test_message_events.py b/tests/hikari/events/test_message_events.py
index 0a0a8f6c..7b6af2ba 100644
--- a/tests/hikari/events/test_message_events.py
+++ b/tests/hikari/events/test_message_events.py
@@ -24,7 +24,6 @@
 import pytest
 
 from hikari import channels
-from hikari import guilds
 from hikari import messages
 from hikari import snowflakes
 from hikari import undefined
@@ -207,7 +206,10 @@ def test_guild_property(self, event):
         event.app.cache.get_guild.assert_called_once_with(342123123)
 
     def test_author_property(self, event):
-        assert event.author is event.message.member
+        assert event.author is event.message.author
+
+    def test_member_property(self, event):
+        assert event.member is event.message.member
 
 
 class TestGuildMessageUpdateEvent:
@@ -224,37 +226,46 @@ def event(self):
             shard=mock.Mock(),
         )
 
-    def test_author_property_when_member_defined(self, event):
-        event.message.member = mock.Mock(spec_set=guilds.Member)
+    def test_author_property(self, event):
+        assert event.author is event.message.author
+
+    def test_member_property(self, event):
+        assert event.member is event.message.member
+
+    def test_member_property_when_member_defined(self, event):
+        event.message.member = mock.Mock()
         event.message.author = undefined.UNDEFINED
 
-        assert event.author is event.message.member
+        assert event.member is event.message.member
 
-    def test_author_property_when_member_none_but_cached(self, event):
+    def test_member_property_when_member_none_but_cached(self, event):
         event.message.member = None
-        event.message.author = mock.Mock(spec_set=users.User, id=1234321)
+        event.message.author = mock.Mock(id=1234321)
         event.message.guild_id = snowflakes.Snowflake(696969)
-        real_member = mock.Mock(spec_set=guilds.Member)
+        real_member = mock.Mock()
         event.app.cache.get_member = mock.Mock(return_value=real_member)
 
-        assert event.author is real_member
+        assert event.member is real_member
 
         event.app.cache.get_member.assert_called_once_with(696969, 1234321)
 
-    def test_author_property_when_member_none_but_author_also_none(self, event):
+    def test_member_property_when_member_none_and_author_none(self, event):
         event.message.author = None
         event.message.member = None
 
-        assert event.author is None
+        assert event.member is None
 
         event.app.cache.get_member.assert_not_called()
 
-    def test_author_property_when_member_none_and_uncached_but_author_defined(self, event):
+    def test_member_property_when_member_none_and_uncached(self, event):
         event.message.member = None
+        event.message.author = mock.Mock(id=1234321)
+        event.message.guild_id = snowflakes.Snowflake(696969)
         event.app.cache.get_member = mock.Mock(return_value=None)
-        event.message.author = mock.Mock(spec_set=users.User)
 
-        assert event.author is event.message.author
+        assert event.member is None
+
+        event.app.cache.get_member.assert_called_once_with(696969, 1234321)
 
     def test_guild_id_property(self, event):
         assert event.guild_id == snowflakes.Snowflake(54123123123)
diff --git a/tests/hikari/hikari_test_helpers.py b/tests/hikari/hikari_test_helpers.py
index ff1eaa8c..fd64d9e9 100644
--- a/tests/hikari/hikari_test_helpers.py
+++ b/tests/hikari/hikari_test_helpers.py
@@ -155,21 +155,6 @@ def decorator(test):
     return decorator
 
 
-def has_sem_open_impl():
-    try:
-        import multiprocessing
-
-        multiprocessing.RLock()
-    except ImportError:
-        return False
-    else:
-        return True
-
-
-def skip_if_no_sem_open(test):
-    return pytest.mark.skipif(not has_sem_open_impl(), reason="Your platform lacks a sem_open implementation")(test)
-
-
 async def idle(for_=REASONABLE_SLEEP_TIME, /):
     await asyncio.sleep(for_)
 
@@ -180,17 +165,6 @@ def ensure_occurs_quickly():
         yield
 
 
-async def gather_all_tasks():
-    """Ensure all created tasks except the current are finished before asserting anything."""
-    await asyncio.gather(*(task for task in asyncio.all_tasks() if task is not asyncio.current_task()))
-
-
-def raiser(ex, should_raise=True):
-    """Stop lints complaining about unreachable code."""
-    if should_raise:
-        raise ex
-
-
 class AsyncContextManagerMock:
     aenter_count = 0
     aexit_count = 0
diff --git a/tests/hikari/impl/test_bot.py b/tests/hikari/impl/test_bot.py
index 23bf67c1..84ee4011 100644
--- a/tests/hikari/impl/test_bot.py
+++ b/tests/hikari/impl/test_bot.py
@@ -162,6 +162,7 @@ def test_init(self):
         voice.assert_called_once_with(bot)
         assert bot._rest is rest.return_value
         rest.assert_called_once_with(
+            cache=bot._cache,
             entity_factory=bot._entity_factory,
             executor=executor,
             http_settings=bot._http_settings,
@@ -278,26 +279,31 @@ def test_check_if_alive(self, bot):
 
     @pytest.mark.asyncio
     async def test_close_when_not_force(self, bot, event_manager):
-        bot._closing_event = mock.Mock(is_set=mock.Mock(return_value=True))
-        bot._closed = False
+        bot._closing_event = mock.Mock(is_set=mock.Mock(return_value=False))
         bot._is_alive = True
 
         await bot.close(force=False)
 
         bot._closing_event.set.assert_called_once_with()
-        assert bot._closed is False
         assert bot._is_alive is True
 
     @pytest.mark.asyncio
-    async def test_close_when_already_closed(self, bot, event_manager):
+    async def test_close_when_not_force_and_closing_event_set(self, bot, event_manager):
         bot._closing_event = mock.Mock(is_set=mock.Mock(return_value=True))
-        bot._closed = True
+        bot._is_alive = True
+
+        await bot.close(force=False)
+
+        bot._closing_event.set.assert_not_called()
+        assert bot._is_alive is True
+
+    @pytest.mark.asyncio
+    async def test_close_when_already_closed(self, bot, event_manager):
+        bot._closing_event = None
         bot._is_alive = True
 
         await bot.close(force=True)
 
-        bot._closing_event.set.assert_called_once_with()
-        assert bot._closed is True
         assert bot._is_alive is True
 
     @pytest.mark.asyncio
@@ -334,8 +340,7 @@ def assert_awaited_once(self):
         event_manager.dispatch = mock.AsyncMock()
         rest.close = AwaitMock()
         voice.close = AwaitMock()
-        bot._closing_event = mock.Mock(is_set=mock.Mock(return_value=False))
-        bot._closed = False
+        bot._closing_event = closing_event = mock.Mock(is_set=mock.Mock(return_value=False))
         bot._is_alive = True
         error = RuntimeError()
         shard0 = mock.Mock(id=0, close=AwaitMock())
@@ -347,8 +352,8 @@ def assert_awaited_once(self):
             await bot.close(force=True)
 
         # Closing event and args
-        bot._closing_event.set.assert_called_once_with()
-        assert bot._closed is True
+        closing_event.set.assert_called_once_with()
+        assert bot._closing_event is None
         assert bot._is_alive is False
 
         # Closing components
@@ -475,7 +480,7 @@ def test_run_with_asyncio_debug(self, bot):
         stack.enter_context(mock.patch.object(bot_impl.BotApp, "start", new=mock.Mock()))
         stack.enter_context(mock.patch.object(bot_impl.BotApp, "join", new=mock.Mock()))
         stack.enter_context(mock.patch.object(bot_impl.BotApp, "close", new=mock.Mock()))
-        loop = stack.enter_context(mock.patch.object(asyncio, "get_event_loop")).return_value
+        loop = stack.enter_context(mock.patch.object(aio, "get_or_make_loop")).return_value
 
         with stack:
             bot.run(close_loop=False, asyncio_debug=True)
@@ -487,7 +492,7 @@ def test_run_with_coroutine_tracking_depth(self, bot):
         stack.enter_context(mock.patch.object(bot_impl.BotApp, "start", new=mock.Mock()))
         stack.enter_context(mock.patch.object(bot_impl.BotApp, "join", new=mock.Mock()))
         stack.enter_context(mock.patch.object(bot_impl.BotApp, "close", new=mock.Mock()))
-        stack.enter_context(mock.patch.object(asyncio, "get_event_loop"))
+        stack.enter_context(mock.patch.object(aio, "get_or_make_loop"))
         coroutine_tracking_depth = stack.enter_context(
             mock.patch.object(sys, "set_coroutine_origin_tracking_depth", side_effect=AttributeError)
         )
@@ -502,7 +507,7 @@ def test_run_with_enable_signal_handlers(self, bot):
         stack.enter_context(mock.patch.object(bot_impl.BotApp, "start", new=mock.Mock()))
         stack.enter_context(mock.patch.object(bot_impl.BotApp, "join", new=mock.Mock()))
         stack.enter_context(mock.patch.object(bot_impl.BotApp, "close", new=mock.Mock()))
-        stack.enter_context(mock.patch.object(asyncio, "get_event_loop"))
+        stack.enter_context(mock.patch.object(aio, "get_or_make_loop"))
         signal_function = stack.enter_context(
             mock.patch.object(signal, "signal", side_effect=[None, AttributeError, None, AttributeError])
         )
@@ -529,7 +534,7 @@ def raise_signal(*args, **kwargs):
         stack.enter_context(mock.patch.object(bot_impl.BotApp, "join", new=raise_signal))
         stack.enter_context(mock.patch.object(bot_impl.BotApp, "close", new=mock.Mock()))
         stack.enter_context(mock.patch.object(bot_impl, "_LOGGER", isEnabledFor=mock.Mock(return_value=logging)))
-        loop = stack.enter_context(mock.patch.object(asyncio, "get_event_loop")).return_value
+        loop = stack.enter_context(mock.patch.object(aio, "get_or_make_loop")).return_value
         set_close_flag = stack.enter_context(mock.patch.object(bot_impl.BotApp, "_set_close_flag", new=mock.Mock()))
         run_coroutine_threadsafe = stack.enter_context(mock.patch.object(asyncio, "run_coroutine_threadsafe"))
 
@@ -545,7 +550,7 @@ def test_run_with_close_passed_executor(self, bot):
         stack.enter_context(mock.patch.object(bot_impl.BotApp, "start", new=mock.Mock()))
         stack.enter_context(mock.patch.object(bot_impl.BotApp, "join", new=mock.Mock()))
         stack.enter_context(mock.patch.object(bot_impl.BotApp, "close", new=mock.Mock()))
-        stack.enter_context(mock.patch.object(asyncio, "get_event_loop"))
+        stack.enter_context(mock.patch.object(aio, "get_or_make_loop"))
         executor = mock.Mock()
         bot._executor = executor
 
@@ -561,7 +566,7 @@ def test_run_with_close_loop(self, bot):
         stack.enter_context(mock.patch.object(bot_impl.BotApp, "join", new=mock.Mock()))
         stack.enter_context(mock.patch.object(bot_impl.BotApp, "close", new=mock.Mock()))
         destroy_loop = stack.enter_context(mock.patch.object(bot_impl.BotApp, "_destroy_loop"))
-        loop = stack.enter_context(mock.patch.object(asyncio, "get_event_loop")).return_value
+        loop = stack.enter_context(mock.patch.object(aio, "get_or_make_loop")).return_value
 
         with stack:
             bot.run(close_loop=True)
@@ -583,7 +588,7 @@ def test_run(self, bot):
         start_function = stack.enter_context(mock.patch.object(bot_impl.BotApp, "start", new=mock.Mock()))
         join_function = stack.enter_context(mock.patch.object(bot_impl.BotApp, "join", new=mock.Mock()))
         close_function = stack.enter_context(mock.patch.object(bot_impl.BotApp, "close", new=mock.Mock()))
-        loop = stack.enter_context(mock.patch.object(asyncio, "get_event_loop")).return_value
+        loop = stack.enter_context(mock.patch.object(aio, "get_or_make_loop")).return_value
 
         with stack:
             bot.run(
@@ -779,7 +784,7 @@ async def test_start_one_shard(self, bot):
         activity = object()
         status = object()
         bot._shards = {}
-        bot._closing_event = mock.Mock()
+        closing_event = mock.Mock()
         shard = mock.Mock()
         shard_obj = shard.return_value
         shard_obj.is_alive = True
@@ -795,6 +800,7 @@ async def test_start_one_shard(self, bot):
                     shard_id=1,
                     shard_count=3,
                     url="https://some.website",
+                    closing_event=closing_event,
                 )
 
                 assert returned is shard_obj
@@ -816,7 +822,7 @@ async def test_start_one_shard(self, bot):
             url="https://some.website",
         )
         assert bot._shards == {1: shard_obj}
-        first_completed.assert_awaited_once_with(shard_obj.start.return_value, bot._closing_event.wait.return_value)
+        first_completed.assert_awaited_once_with(shard_obj.start.return_value, closing_event.wait.return_value)
 
     @pytest.mark.asyncio
     async def test_start_one_shard_when_not_alive(self, bot):
@@ -839,6 +845,7 @@ async def test_start_one_shard_when_not_alive(self, bot):
                         shard_id=1,
                         shard_count=3,
                         url="https://some.website",
+                        closing_event=bot._closing_event,
                     )
 
     @pytest.mark.parametrize("activity", [undefined.UNDEFINED, None])
diff --git a/tests/hikari/impl/test_cache.py b/tests/hikari/impl/test_cache.py
index 02809ab6..c59ca1cf 100644
--- a/tests/hikari/impl/test_cache.py
+++ b/tests/hikari/impl/test_cache.py
@@ -83,6 +83,64 @@ def test_clear(self, cache_impl):
 
         cache_impl._create_cache.assert_called_once_with()
 
+    def test_clear_dm_channel_ids(self, cache_impl):
+        cache_impl._dm_channel_entries = collections.FreezableDict({123: 5423, 23123: 54123123})
+
+        result = cache_impl.clear_dm_channel_ids()
+
+        assert result == {123: 5423, 23123: 54123123}
+        assert cache_impl._dm_channel_entries == {}
+
+    def test_delete_dm_channel_id(self, cache_impl):
+        cache_impl._dm_channel_entries = collections.FreezableDict({54123: 2123123, 5434: 1234})
+
+        result = cache_impl.delete_dm_channel_id(54123)
+
+        assert result == 2123123
+        assert cache_impl._dm_channel_entries == {5434: 1234}
+
+    def test_delete_dm_channel_id_for_unknown_user(self, cache_impl):
+        cache_impl._dm_channel_entries = collections.FreezableDict({54123: 2123123, 5434: 1234})
+
+        result = cache_impl.delete_dm_channel_id(65234123123)
+
+        assert result is None
+        assert cache_impl._dm_channel_entries == {54123: 2123123, 5434: 1234}
+
+    def test_get_dm_channel_id(self, cache_impl):
+        cache_impl._dm_channel_entries = collections.FreezableDict({24123123: 453123, 5423: 123, 653: 1223})
+
+        assert cache_impl.get_dm_channel_id(5423) == 123
+
+    def test_get_dm_channel_id_for_unknown_user(self, cache_impl):
+        cache_impl._dm_channel_entries = collections.FreezableDict({24123123: 453123, 5423: 123, 653: 1223})
+
+        assert cache_impl.get_dm_channel_id(65656565) is None
+
+    def test_get_dm_channel_ids_view(self, cache_impl):
+        cache_impl._dm_channel_entries = collections.FreezableDict({222: 333, 643: 213, 54234: 1231321})
+
+        assert cache_impl.get_dm_channel_ids_view() == {222: 333, 643: 213, 54234: 1231321}
+
+    def test_set_dm_channel_id(self, cache_impl):
+        cache_impl._user_entries = collections.FreezableDict({43123123: object()})
+
+        cache_impl.set_dm_channel_id(StubModel(43123123), StubModel(12222))
+
+        assert cache_impl._dm_channel_entries == {43123123: 12222}
+
+    def test_set_dm_channel_id_when_user_not_known(self, cache_impl):
+        cache_impl.set_dm_channel_id(StubModel(43123123), StubModel(12222))
+
+        assert cache_impl._dm_channel_entries == {}
+
+    def test_set_dm_channel_id_when_not_enabled(self, cache_impl):
+        cache_impl._user_entries = collections.FreezableDict({43123123: object()})
+        cache_impl._settings.components = 0
+        cache_impl.set_dm_channel_id(StubModel(43123123), StubModel(12222))
+
+        assert cache_impl._dm_channel_entries == {}
+
     def test__build_emoji(self, cache_impl):
         mock_user = mock.MagicMock(users.User)
         emoji_data = cache_utilities.KnownCustomEmojiData(
@@ -1938,7 +1996,23 @@ def test__garbage_collect_user_for_known_unreferenced_user(self, cache_impl):
 
         cache_impl._garbage_collect_user(mock_user, decrement=1)
 
-        assert dict(cache_impl._user_entries) == {snowflakes.Snowflake(645234): mock_other_user}
+        assert cache_impl._user_entries == {snowflakes.Snowflake(645234): mock_other_user}
+
+    def test__garbage_collect_user_for_known_unreferenced_user_removes_cached_dm_channelo(self, cache_impl):
+        mock_user = cache_utilities.RefCell(mock.Mock(id=snowflakes.Snowflake(21231234)), ref_count=1)
+        cache_impl._dm_channel_entries = collections.FreezableDict({21231234: 123123123})
+        mock_other_user = mock.Mock(cache_utilities.RefCell, ref_count=1)
+        cache_impl._user_entries = collections.FreezableDict(
+            {
+                snowflakes.Snowflake(21231234): mock_user,
+                snowflakes.Snowflake(645234): mock_other_user,
+            }
+        )
+
+        cache_impl._garbage_collect_user(mock_user, decrement=1)
+
+        assert cache_impl._user_entries == {snowflakes.Snowflake(645234): mock_other_user}
+        assert cache_impl._dm_channel_entries == {}
 
     def test_garbage_collect_user_for_referenced_user(self, cache_impl):
         mock_user = cache_utilities.RefCell(mock.Mock(id=snowflakes.Snowflake(21231234)), ref_count=2)
diff --git a/tests/hikari/impl/test_event_manager_base.py b/tests/hikari/impl/test_event_manager_base.py
index 00f427bd..e2396b6b 100644
--- a/tests/hikari/impl/test_event_manager_base.py
+++ b/tests/hikari/impl/test_event_manager_base.py
@@ -165,7 +165,7 @@ async def test():
             ...
 
         with mock.patch.object(event_manager_base.EventManagerBase, "_check_intents") as check:
-            assert event_manager.subscribe(member_events.MemberCreateEvent, test, _nested=1) == test
+            event_manager.subscribe(member_events.MemberCreateEvent, test, _nested=1)
 
         assert event_manager._listeners == {member_events.MemberCreateEvent: [test]}
         check.assert_called_once_with(member_events.MemberCreateEvent, 1)
@@ -180,7 +180,7 @@ async def test2():
         event_manager._listeners[member_events.MemberCreateEvent] = [test2]
 
         with mock.patch.object(event_manager_base.EventManagerBase, "_check_intents") as check:
-            assert event_manager.subscribe(member_events.MemberCreateEvent, test, _nested=2) == test
+            event_manager.subscribe(member_events.MemberCreateEvent, test, _nested=2)
 
         assert event_manager._listeners == {member_events.MemberCreateEvent: [test2, test]}
         check.assert_called_once_with(member_events.MemberCreateEvent, 2)
diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py
index 3c755b94..9e23d8c5 100644
--- a/tests/hikari/impl/test_rest.py
+++ b/tests/hikari/impl/test_rest.py
@@ -123,6 +123,11 @@ def test_scopes_property(self):
 
         assert token.scopes == (123, 5643)
 
+    def test_token_type_property(self):
+        token = rest.ClientCredentialsStrategy(client=123, client_secret="123123123", scopes=[])
+
+        assert token.token_type is applications.TokenType.BEARER
+
     async def test_acquire_on_new_instance(self, mock_token):
         mock_rest = mock.Mock(authorize_client_credentials_token=mock.AsyncMock(return_value=mock_token))
 
@@ -287,6 +292,7 @@ def test_acquire(self, rest_app):
             rest_app.acquire(token="token", token_type="Type")
 
         mock_client.assert_called_once_with(
+            cache=None,
             entity_factory=_entity_factory(),
             executor=rest_app._executor,
             http_settings=rest_app._http_settings,
@@ -297,6 +303,30 @@ def test_acquire(self, rest_app):
             rest_url=rest_app._url,
         )
 
+    def test_acquire_defaults_to_bearer_for_a_string_token(self, rest_app):
+        mock_event_loop = object()
+        rest_app._event_loop = mock_event_loop
+
+        stack = contextlib.ExitStack()
+        _entity_factory = stack.enter_context(mock.patch.object(entity_factory, "EntityFactoryImpl"))
+        mock_client = stack.enter_context(mock.patch.object(rest, rest.RESTClientImpl.__qualname__))
+        stack.enter_context(mock.patch.object(asyncio, "get_running_loop", return_value=mock_event_loop))
+
+        with stack:
+            rest_app.acquire(token="token")
+
+        mock_client.assert_called_once_with(
+            cache=None,
+            entity_factory=_entity_factory(),
+            executor=rest_app._executor,
+            http_settings=rest_app._http_settings,
+            max_rate_limit=float("inf"),
+            proxy_settings=rest_app._proxy_settings,
+            token="token",
+            token_type=applications.TokenType.BEARER,
+            rest_url=rest_app._url,
+        )
+
 
 ##################
 # RESTClientImpl #
@@ -309,8 +339,14 @@ def rest_client_class():
 
 
 @pytest.fixture()
-def rest_client(rest_client_class):
+def mock_cache():
+    return mock.Mock()
+
+
+@pytest.fixture()
+def rest_client(rest_client_class, mock_cache):
     obj = rest_client_class(
+        cache=mock_cache,
         http_settings=mock.Mock(spec=config.HTTPSettings),
         max_rate_limit=float("inf"),
         proxy_settings=mock.Mock(spec=config.ProxySettings),
@@ -376,6 +412,7 @@ class TestRESTClientImpl:
     def test__init__passes_max_rate_limit(self):
         with mock.patch.object(buckets, "RESTBucketManager") as bucket:
             rest.RESTClientImpl(
+                cache=None,
                 http_settings=mock.Mock(),
                 max_rate_limit=float("inf"),
                 proxy_settings=mock.Mock(),
@@ -391,6 +428,7 @@ def test__init__passes_max_rate_limit(self):
     def test__init__when_token_strategy_passed_with_token_type(self):
         with pytest.raises(ValueError, match="Token type should be handled by the token strategy"):
             rest.RESTClientImpl(
+                cache=None,
                 http_settings=mock.Mock(),
                 max_rate_limit=float("inf"),
                 proxy_settings=mock.Mock(),
@@ -401,8 +439,26 @@ def test__init__when_token_strategy_passed_with_token_type(self):
                 entity_factory=None,
             )
 
+    def test__init__when_token_strategy_passed(self):
+        mock_strategy = mock.Mock(rest_api.TokenStrategy)
+        obj = rest.RESTClientImpl(
+            cache=None,
+            http_settings=mock.Mock(),
+            max_rate_limit=float("inf"),
+            proxy_settings=mock.Mock(),
+            token=mock_strategy,
+            token_type=None,
+            rest_url=None,
+            executor=None,
+            entity_factory=None,
+        )
+
+        assert obj._token is mock_strategy
+        assert obj._token_type is mock_strategy.token_type
+
     def test__init__when_token_is_None_sets_token_to_None(self):
         obj = rest.RESTClientImpl(
+            cache=None,
             http_settings=mock.Mock(),
             max_rate_limit=float("inf"),
             proxy_settings=mock.Mock(),
@@ -413,9 +469,11 @@ def test__init__when_token_is_None_sets_token_to_None(self):
             entity_factory=None,
         )
         assert obj._token is None
+        assert obj._token_type is None
 
     def test__init__when_token_and_token_type_is_not_None_generates_token_with_type(self):
         obj = rest.RESTClientImpl(
+            cache=None,
             http_settings=mock.Mock(),
             max_rate_limit=float("inf"),
             proxy_settings=mock.Mock(),
@@ -426,9 +484,25 @@ def test__init__when_token_and_token_type_is_not_None_generates_token_with_type(
             entity_factory=None,
         )
         assert obj._token == "Type some_token"
+        assert obj._token_type == "Type"
+
+    def test__init__when_token_provided_as_string_without_type(self):
+        with pytest.raises(ValueError, match="Token type required when a str is passed for `token`"):
+            rest.RESTClientImpl(
+                cache=None,
+                http_settings=mock.Mock(),
+                max_rate_limit=float("inf"),
+                proxy_settings=mock.Mock(),
+                token="some_token",
+                token_type=None,
+                rest_url=None,
+                executor=None,
+                entity_factory=None,
+            )
 
     def test__init__when_rest_url_is_None_generates_url_using_default_url(self):
         obj = rest.RESTClientImpl(
+            cache=None,
             http_settings=mock.Mock(),
             max_rate_limit=float("inf"),
             proxy_settings=mock.Mock(),
@@ -442,6 +516,7 @@ def test__init__when_rest_url_is_None_generates_url_using_default_url(self):
 
     def test__init__when_rest_url_is_not_None_generates_url_using_given_url(self):
         obj = rest.RESTClientImpl(
+            cache=None,
             http_settings=mock.Mock(),
             max_rate_limit=float("inf"),
             proxy_settings=mock.Mock(),
@@ -469,11 +544,16 @@ def test_http_settings_property(self, rest_client):
         rest_client._http_settings = mock_http_settings
         assert rest_client.http_settings is mock_http_settings
 
-    def test_proxy_settings(self, rest_client):
+    def test_proxy_settings_property(self, rest_client):
         mock_proxy_settings = object()
         rest_client._proxy_settings = mock_proxy_settings
         assert rest_client.proxy_settings is mock_proxy_settings
 
+    def test_token_type_property(self, rest_client):
+        mock_type = object()
+        rest_client._token_type = mock_type
+        assert rest_client.token_type is mock_type
+
     def test__acquire_client_session_when_None(self, rest_client):
         client_session_mock = object()
         rest_client._acquire_tcp_connector = mock.Mock()
@@ -1285,6 +1365,29 @@ async def test_fetch_channel(self, rest_client):
         rest_client._request.assert_awaited_once_with(expected_route)
         rest_client._entity_factory.deserialize_channel.assert_called_once_with(rest_client._request.return_value)
 
+    async def test_fetch_channel_with_dm_channel_when_cacheful(self, rest_client, mock_cache):
+        expected_route = routes.GET_CHANNEL.compile(channel=123)
+        mock_object = mock.Mock(spec=channels.DMChannel, type=channels.ChannelType.DM)
+        rest_client._entity_factory.deserialize_channel = mock.Mock(return_value=mock_object)
+        rest_client._request = mock.AsyncMock(return_value={"payload": "NO"})
+
+        assert await rest_client.fetch_channel(StubModel(123)) == mock_object
+        rest_client._request.assert_awaited_once_with(expected_route)
+        rest_client._entity_factory.deserialize_channel.assert_called_once_with(rest_client._request.return_value)
+        mock_cache.set_dm_channel_id.assert_called_once_with(mock_object.recipient.id, mock_object.id)
+
+    async def test_fetch_channel_with_dm_channel_when_cacheless(self, rest_client, mock_cache):
+        expected_route = routes.GET_CHANNEL.compile(channel=123)
+        mock_object = mock.Mock(spec=channels.DMChannel, type=channels.ChannelType.DM)
+        rest_client._cache = None
+        rest_client._entity_factory.deserialize_channel = mock.Mock(return_value=mock_object)
+        rest_client._request = mock.AsyncMock(return_value={"payload": "NO"})
+
+        assert await rest_client.fetch_channel(StubModel(123)) == mock_object
+        rest_client._request.assert_awaited_once_with(expected_route)
+        rest_client._entity_factory.deserialize_channel.assert_called_once_with(rest_client._request.return_value)
+        mock_cache.set_dm_channel_id.assert_not_called()
+
     async def test_edit_channel(self, rest_client):
         expected_route = routes.PATCH_CHANNEL.compile(channel=123)
         mock_object = mock.Mock()
@@ -2013,16 +2116,31 @@ async def test_leave_guild(self, rest_client):
         await rest_client.leave_guild(StubModel(123))
         rest_client._request.assert_awaited_once_with(expected_route)
 
-    async def test_create_dm_channel(self, rest_client):
-        dm_channel = StubModel(123)
+    async def test_create_dm_channel(self, rest_client, mock_cache):
+        dm_channel = StubModel(43234)
+        user = StubModel(123)
         expected_route = routes.POST_MY_CHANNELS.compile()
         expected_json = {"recipient_id": "123"}
-        rest_client._request = mock.AsyncMock(return_value={"id": "123"})
+        rest_client._request = mock.AsyncMock(return_value={"id": "43234"})
+        rest_client._entity_factory.deserialize_dm = mock.Mock(return_value=dm_channel)
+
+        assert await rest_client.create_dm_channel(user) == dm_channel
+        rest_client._request.assert_awaited_once_with(expected_route, json=expected_json)
+        rest_client._entity_factory.deserialize_dm.assert_called_once_with({"id": "43234"})
+        mock_cache.set_dm_channel_id.assert_called_once_with(user, dm_channel.id)
+
+    async def test_create_dm_channel_when_cacheless(self, rest_client, mock_cache):
+        rest_client._cache = None
+        dm_channel = StubModel(43234)
+        expected_route = routes.POST_MY_CHANNELS.compile()
+        expected_json = {"recipient_id": "123"}
+        rest_client._request = mock.AsyncMock(return_value={"id": "43234"})
         rest_client._entity_factory.deserialize_dm = mock.Mock(return_value=dm_channel)
 
         assert await rest_client.create_dm_channel(StubModel(123)) == dm_channel
         rest_client._request.assert_awaited_once_with(expected_route, json=expected_json)
-        rest_client._entity_factory.deserialize_dm.assert_called_once_with({"id": "123"})
+        rest_client._entity_factory.deserialize_dm.assert_called_once_with({"id": "43234"})
+        mock_cache.set_dm_channel_id.assert_not_called()
 
     async def test_fetch_application(self, rest_client):
         application = StubModel(123)
diff --git a/tests/hikari/impl/test_shard.py b/tests/hikari/impl/test_shard.py
index dbe32c16..e637b4b5 100644
--- a/tests/hikari/impl/test_shard.py
+++ b/tests/hikari/impl/test_shard.py
@@ -323,7 +323,7 @@ def __init__(self):
                 url="https://some.url",
                 log_filterer=log_filterer,
             ):
-                hikari_test_helpers.raiser(errors.GatewayError("some reason"))
+                raise errors.GatewayError("some reason")
 
         mock_websocket.send_close.assert_awaited_once_with(
             code=errors.ShardCloseCode.UNEXPECTED_CONDITION, message=b"unexpected fatal client error :-("
@@ -363,7 +363,7 @@ def __init__(self):
                 url="https://some.url",
                 log_filterer=log_filterer,
             ):
-                hikari_test_helpers.raiser(ValueError("testing"))
+                raise ValueError("testing")
 
         mock_websocket.send_close.assert_awaited_once_with(
             code=errors.ShardCloseCode.UNEXPECTED_CONDITION, message=b"unexpected fatal client error :-("
@@ -626,17 +626,19 @@ def test_intents_property(self, client):
         client._intents = intents
         assert client.intents is intents
 
-    @pytest.mark.parametrize(
-        ("run_task", "expected"),
-        [
-            (None, False),
-            (asyncio.get_event_loop().create_future(), True),
-            (aio.completed_future(), False),
-        ],
-    )
-    def test_is_alive_property(self, run_task, expected, client):
-        client._run_task = run_task
-        assert client.is_alive is expected
+    def test_is_alive_property(self, client):
+        client._run_task = None
+        assert client.is_alive is False
+
+    @pytest.mark.asyncio()
+    async def test_is_alive_property_with_active_future(self, client):
+        client._run_task = asyncio.get_running_loop().create_future()
+        assert client.is_alive is True
+
+    @pytest.mark.asyncio()
+    async def test_is_alive_property_with_finished_future(self, client):
+        client._run_task = aio.completed_future()
+        assert client.is_alive is False
 
     def test_shard_count_property(self, client):
         client._shard_count = 69
diff --git a/tests/hikari/internal/test_aio.py b/tests/hikari/internal/test_aio.py
index fc480215..b1545fcb 100644
--- a/tests/hikari/internal/test_aio.py
+++ b/tests/hikari/internal/test_aio.py
@@ -21,6 +21,7 @@
 # SOFTWARE.
 import asyncio
 
+import mock.mock
 import pytest
 
 from hikari.internal import aio
@@ -379,3 +380,34 @@ async def test_cancels_all_if_cancelled(self, event_loop):
             await waiter
         assert waiter.cancelled(), "future was forcefully cancelled?"
         assert waiter.done(), "asyncio.CancelledError not raised?"
+
+
+def test_get_or_make_loop():
+    mock_loop = mock.Mock(asyncio.AbstractEventLoop, is_closed=mock.Mock(return_value=False))
+    asyncio.set_event_loop(mock_loop)
+
+    assert aio.get_or_make_loop() is mock_loop
+
+
+def test_get_or_make_loop_handles_runtime_error():
+    asyncio.set_event_loop(None)
+    mock_loop = mock.Mock(asyncio.AbstractEventLoop)
+
+    with mock.patch.object(asyncio, "new_event_loop", return_value=mock_loop) as new_event_loop:
+        assert aio.get_or_make_loop() is mock_loop
+
+        new_event_loop.assert_called_once_with()
+
+    assert asyncio.get_event_loop() is mock_loop
+
+
+def test_get_or_make_loop_handles_closed_loop():
+    asyncio.set_event_loop(mock.Mock(asyncio.AbstractEventLoop, is_closed=mock.Mock(return_value=True)))
+    mock_loop = mock.Mock(asyncio.AbstractEventLoop)
+
+    with mock.patch.object(asyncio, "new_event_loop", return_value=mock_loop) as new_event_loop:
+        assert aio.get_or_make_loop() is mock_loop
+
+        new_event_loop.assert_called_once_with()
+
+    assert asyncio.get_event_loop() is mock_loop
diff --git a/tests/hikari/internal/test_time.py b/tests/hikari/internal/test_time.py
index 563e43fe..5472d930 100644
--- a/tests/hikari/internal/test_time.py
+++ b/tests/hikari/internal/test_time.py
@@ -41,7 +41,7 @@ def test_parse_iso_8601_date_with_negative_timezone():
     assert offset == datetime.timedelta(hours=-2, minutes=-30)
 
 
-def test_parse_iso_8601_date_with_positive_timezone():
+def test_slow_parse_iso_8601_date_with_positive_timezone():
     string = "2019-10-10T05:22:33.023456+02:30"
     date = time.iso8601_datetime_string_to_datetime(string)
     assert date.year == 2019
@@ -93,6 +93,19 @@ def test_parse_iso_8601_date_with_no_fraction():
     assert date.microsecond == 0
 
 
+def test_speedup_replaces_python_version_when_available():
+    try:
+        import ciso8601
+
+        assert time.fast_iso8601_datetime_string_to_datetime is ciso8601.parse_rfc3339
+        assert time.iso8601_datetime_string_to_datetime is ciso8601.parse_rfc3339
+    except ImportError:
+        # No speedups, test pure version has been setup correctly
+
+        assert time.fast_iso8601_datetime_string_to_datetime is None
+        assert time.iso8601_datetime_string_to_datetime is time.slow_iso8601_datetime_string_to_datetime
+
+
 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)
diff --git a/tests/hikari/test_applications.py b/tests/hikari/test_applications.py
index 327409f3..5368fbc3 100644
--- a/tests/hikari/test_applications.py
+++ b/tests/hikari/test_applications.py
@@ -57,10 +57,6 @@ def test_flags_property(self, model):
     def test_id_property(self, model):
         assert model.id is model.user.id
 
-    def test_id_setter(self, model):
-        with pytest.raises(TypeError, match="Cannot mutate the ID of a member"):
-            model.id = 42
-
     def test_is_bot_property(self, model):
         assert model.is_bot is model.user.is_bot
 
@@ -73,14 +69,6 @@ def test_mention_property(self, model):
     def test_username_property(self, model):
         assert model.username is model.user.username
 
-    @pytest.mark.asyncio
-    async def test_fetch_dm_channel(self, model):
-        model.user.fetch_dm_channel = mock.AsyncMock()
-
-        result = await model.fetch_dm_channel()
-        assert result is model.user.fetch_dm_channel.return_value
-        model.user.fetch_dm_channel.assert_awaited_once()
-
     def test_str_operator(self):
         mock_team_member = mock.Mock(
             applications.TeamMember, user=mock.Mock(users.User, __str__=mock.Mock(return_value="mario#1234"))
diff --git a/tests/hikari/test_channels.py b/tests/hikari/test_channels.py
index 2682a46f..0b65028f 100644
--- a/tests/hikari/test_channels.py
+++ b/tests/hikari/test_channels.py
@@ -214,6 +214,7 @@ async def test_send(self, model):
         model.app.rest.create_message = mock.AsyncMock()
         mock_attachment = object()
         mock_embed = object()
+        mock_embeds = object()
         mock_attachments = [object(), object(), object()]
         mock_reply = object()
 
@@ -224,6 +225,7 @@ async def test_send(self, model):
             attachment=mock_attachment,
             attachments=mock_attachments,
             embed=mock_embed,
+            embeds=mock_embeds,
             reply=mock_reply,
             mentions_everyone=False,
             user_mentions=[123, 456],
@@ -239,6 +241,7 @@ async def test_send(self, model):
             attachment=mock_attachment,
             attachments=mock_attachments,
             embed=mock_embed,
+            embeds=mock_embeds,
             reply=mock_reply,
             mentions_everyone=False,
             user_mentions=[123, 456],
@@ -269,18 +272,14 @@ def model(self, mock_app):
             parent_id=None,
         )
 
-    @pytest.mark.parametrize("error", [TypeError, AttributeError, NameError])
-    def test_shard_id_property_when_guild_id_error_raised(self, model, error):
-        class BrokenApp:
-            def __getattr__(self, name):
-                if name == "shard_count":
-                    raise error
-                return mock.Mock()
-
-        model.app = BrokenApp()
+    def test_shard_id_property_when_not_shard_aware(self, model):
+        model.app = None
 
         assert model.shard_id is None
 
     def test_shard_id_property_when_guild_id_is_not_None(self, model):
         model.app.shard_count = 3
         assert model.shard_id == 2
+
+    def test_mention_property(self, model):
+        assert model.mention == "<#69420>"
diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py
index 47c630c3..37f75218 100644
--- a/tests/hikari/test_guilds.py
+++ b/tests/hikari/test_guilds.py
@@ -52,6 +52,9 @@ def model(self, mock_app):
     def test_str_operator(self, model):
         assert str(model) == "The Big Cool"
 
+    def test_mention_property(self, model):
+        assert model.mention == "<@&1106913972>"
+
 
 def test_PartialApplication_str_operator():
     mock_application = mock.Mock(guilds.PartialApplication)
@@ -175,10 +178,6 @@ def test_app_property(self, model, mock_user):
     def test_id_property(self, model, mock_user):
         assert model.id is mock_user.id
 
-    def test_id_setter_property(self, model):
-        with pytest.raises(TypeError):
-            model.id = 456
-
     def test_username_property(self, model, mock_user):
         assert model.username is mock_user.username
 
@@ -205,13 +204,6 @@ def test_make_avatar_url(self, model, mock_user):
         mock_user.make_avatar_url.assert_called_once_with(ext="png", size=4096)
         assert result is mock_user.make_avatar_url.return_value
 
-    @pytest.mark.asyncio
-    async def test_fetch_dm_channel(self, model, mock_user):
-        mock_user.fetch_dm_channel = mock.AsyncMock()
-        result = await model.fetch_dm_channel()
-        mock_user.fetch_dm_channel.assert_awaited_once_with()
-        assert result is mock_user.fetch_dm_channel.return_value
-
     @pytest.mark.asyncio
     async def test_fetch_self(self, model):
         model.user.app.rest.fetch_member = mock.AsyncMock()
@@ -482,6 +474,38 @@ def model(self, mock_app):
             system_channel_flags=guilds.GuildSystemChannelFlag.SUPPRESS_PREMIUM_SUBSCRIPTION,
         )
 
+    def test_channels(self, model):
+        assert model.channels is model.app.cache.get_guild_channels_view_for_guild.return_value
+        model.app.cache.get_guild_channels_view_for_guild.assert_called_once_with(123)
+
+    def test_channels_when_no_cache_trait(self, model):
+        model.app = object()
+        assert model.channels == {}
+
+    def test_members(self, model):
+        assert model.members is model.app.cache.get_members_view_for_guild.return_value
+        model.app.cache.get_members_view_for_guild.assert_called_once_with(123)
+
+    def test_members_when_no_cache_trait(self, model):
+        model.app = object()
+        assert model.members == {}
+
+    def test_presences(self, model):
+        assert model.presences is model.app.cache.get_presences_view_for_guild.return_value
+        model.app.cache.get_presences_view_for_guild.assert_called_once_with(123)
+
+    def test_presences_when_no_cache_trait(self, model):
+        model.app = object()
+        assert model.presences == {}
+
+    def test_voice_states(self, model):
+        assert model.voice_states is model.app.cache.get_voice_states_view_for_guild.return_value
+        model.app.cache.get_voice_states_view_for_guild.assert_called_once_with(123)
+
+    def test_voice_states_when_no_cache_trait(self, model):
+        model.app = object()
+        assert model.voice_states == {}
+
     def test_splash_url(self, model):
         splash = object()
 
@@ -558,6 +582,54 @@ def test_make_banner_url_when_no_hash(self, model):
         model.banner_hash = None
         assert model.make_banner_url(ext="png", size=2048) is None
 
+    def test_get_channel(self, model):
+        assert model.get_channel(456) is model.app.cache.get_guild_channel.return_value
+        model.app.cache.get_guild_channel.assert_called_once_with(456)
+
+    def test_get_channel_when_no_cache_trait(self, model):
+        model.app = object()
+        assert model.get_channel(456) is None
+
+    def test_get_member(self, model):
+        assert model.get_member(456) is model.app.cache.get_member.return_value
+        model.app.cache.get_member.assert_called_once_with(123, 456)
+
+    def test_get_member_when_no_cache_trait(self, model):
+        model.app = object()
+        assert model.get_member(456) is None
+
+    def test_get_presence(self, model):
+        assert model.get_presence(456) is model.app.cache.get_presence.return_value
+        model.app.cache.get_presence.assert_called_once_with(123, 456)
+
+    def test_get_presence_when_no_cache_trait(self, model):
+        model.app = object()
+        assert model.get_presence(456) is None
+
+    def test_get_voice_state(self, model):
+        assert model.get_voice_state(456) is model.app.cache.get_voice_state.return_value
+        model.app.cache.get_voice_state.assert_called_once_with(123, 456)
+
+    def test_get_voice_state_when_no_cache_trait(self, model):
+        model.app = object()
+        assert model.get_voice_state(456) is None
+
+    def test_get_my_member_when_not_shardaware(self, model):
+        model.app = object()
+        assert model.get_my_member() is None
+
+    def test_get_my_member_when_no_me(self, model):
+        model.app.me = None
+        assert model.get_my_member() is None
+
+    def test_get_my_member(self, model):
+        model.app.me = mock.Mock(id=123)
+
+        with mock.patch.object(guilds.Guild, "get_member") as get_member:
+            assert model.get_my_member() is get_member.return_value
+
+        get_member.assert_called_once_with(123)
+
 
 class TestRestGuild:
     @pytest.fixture()
@@ -651,14 +723,6 @@ def model(self, mock_app):
             nsfw_level=guilds.GuildNSFWLevel.AGE_RESTRICTED,
         )
 
-    def test_channels(self, model):
-        assert model.channels is model.app.cache.get_guild_channels_view_for_guild.return_value
-        model.app.cache.get_guild_channels_view_for_guild.assert_called_once_with(123)
-
-    def test_channels_when_no_cache_trait(self, model):
-        model.app = object()
-        assert model.channels == {}
-
     def test_emojis(self, model):
         assert model.emojis is model.app.cache.get_emojis_view_for_guild.return_value
         model.app.cache.get_emojis_view_for_guild.assert_called_once_with(123)
@@ -667,22 +731,6 @@ def test_emojis_when_no_cache_trait(self, model):
         model.app = object()
         assert model.emojis == {}
 
-    def test_members(self, model):
-        assert model.members is model.app.cache.get_members_view_for_guild.return_value
-        model.app.cache.get_members_view_for_guild.assert_called_once_with(123)
-
-    def test_members_when_no_cache_trait(self, model):
-        model.app = object()
-        assert model.members == {}
-
-    def test_presences(self, model):
-        assert model.presences is model.app.cache.get_presences_view_for_guild.return_value
-        model.app.cache.get_presences_view_for_guild.assert_called_once_with(123)
-
-    def test_presences_when_no_cache_trait(self, model):
-        model.app = object()
-        assert model.presences == {}
-
     def test_roles(self, model):
         assert model.roles is model.app.cache.get_roles_view_for_guild.return_value
         model.app.cache.get_roles_view_for_guild.assert_called_once_with(123)
@@ -691,22 +739,6 @@ def test_roles_when_no_cache_trait(self, model):
         model.app = object()
         assert model.roles == {}
 
-    def test_voice_states(self, model):
-        assert model.voice_states is model.app.cache.get_voice_states_view_for_guild.return_value
-        model.app.cache.get_voice_states_view_for_guild.assert_called_once_with(123)
-
-    def test_voice_states_when_no_cache_trait(self, model):
-        model.app = object()
-        assert model.voice_states == {}
-
-    def test_get_channel(self, model):
-        assert model.get_channel(456) is model.app.cache.get_guild_channel.return_value
-        model.app.cache.get_guild_channel.assert_called_once_with(456)
-
-    def test_get_channel_when_no_cache_trait(self, model):
-        model.app = object()
-        assert model.get_channel(456) is None
-
     def test_get_emoji(self, model):
         assert model.get_emoji(456) is model.app.cache.get_emoji.return_value
         model.app.cache.get_emoji.assert_called_once_with(456)
@@ -715,22 +747,6 @@ def test_get_emoji_when_no_cache_trait(self, model):
         model.app = object()
         assert model.get_emoji(456) is None
 
-    def test_get_member(self, model):
-        assert model.get_member(456) is model.app.cache.get_member.return_value
-        model.app.cache.get_member.assert_called_once_with(123, 456)
-
-    def test_get_member_when_no_cache_trait(self, model):
-        model.app = object()
-        assert model.get_member(456) is None
-
-    def test_get_presence(self, model):
-        assert model.get_presence(456) is model.app.cache.get_presence.return_value
-        model.app.cache.get_presence.assert_called_once_with(123, 456)
-
-    def test_get_presence_when_no_cache_trait(self, model):
-        model.app = object()
-        assert model.get_presence(456) is None
-
     def test_get_role(self, model):
         assert model.get_role(456) is model.app.cache.get_role.return_value
         model.app.cache.get_role.assert_called_once_with(456)
@@ -738,27 +754,3 @@ def test_get_role(self, model):
     def test_get_role_when_no_cache_trait(self, model):
         model.app = object()
         assert model.get_role(456) is None
-
-    def test_get_voice_state(self, model):
-        assert model.get_voice_state(456) is model.app.cache.get_voice_state.return_value
-        model.app.cache.get_voice_state.assert_called_once_with(123, 456)
-
-    def test_get_voice_state_when_no_cache_trait(self, model):
-        model.app = object()
-        assert model.get_voice_state(456) is None
-
-    def test_get_my_member_when_not_shardaware(self, model):
-        model.app = object()
-        assert model.get_my_member() is None
-
-    def test_get_my_member_when_no_me(self, model):
-        model.app.me = None
-        assert model.get_my_member() is None
-
-    def test_get_my_member(self, model):
-        model.app.me = mock.Mock(id=123)
-
-        with mock.patch.object(guilds.GatewayGuild, "get_member") as get_member:
-            assert model.get_my_member() is get_member.return_value
-
-        get_member.assert_called_once_with(123)
diff --git a/tests/hikari/test_messages.py b/tests/hikari/test_messages.py
index 7e065683..2e27e7c4 100644
--- a/tests/hikari/test_messages.py
+++ b/tests/hikari/test_messages.py
@@ -28,6 +28,7 @@
 from hikari import guilds
 from hikari import messages
 from hikari import snowflakes
+from hikari import undefined
 from hikari import urls
 from hikari import users
 from hikari.internal import routes
@@ -164,11 +165,13 @@ async def test_edit(self, message):
         message.id = 123
         message.channel_id = 456
         embed = object()
+        embeds = [object(), object()]
         attachment = object()
         roles = [object()]
         await message.edit(
             content="test content",
             embed=embed,
+            embeds=embeds,
             attachment=attachment,
             attachments=[attachment, attachment],
             replace_attachments=True,
@@ -183,6 +186,7 @@ async def test_edit(self, message):
             channel=456,
             content="test content",
             embed=embed,
+            embeds=embeds,
             attachment=attachment,
             attachments=[attachment, attachment],
             replace_attachments=True,
@@ -198,6 +202,7 @@ async def test_respond(self, message):
         message.id = 123
         message.channel_id = 456
         embed = object()
+        embeds = [object(), object()]
         roles = [object()]
         attachment = object()
         attachments = [object()]
@@ -205,6 +210,7 @@ async def test_respond(self, message):
         await message.respond(
             content="test content",
             embed=embed,
+            embeds=embeds,
             attachment=attachment,
             attachments=attachments,
             nonce="nonce",
@@ -219,6 +225,7 @@ async def test_respond(self, message):
             channel=456,
             content="test content",
             embed=embed,
+            embeds=embeds,
             attachment=attachment,
             attachments=attachments,
             nonce="nonce",
@@ -235,12 +242,14 @@ async def test_respond_when_reply_is_True(self, message):
         message.id = 123
         message.channel_id = 456
         embed = object()
+        embeds = [object()]
         roles = [object()]
         attachment = object()
         attachments = [object()]
         await message.respond(
             content="test content",
             embed=embed,
+            embeds=embeds,
             attachment=attachment,
             attachments=attachments,
             nonce="nonce",
@@ -255,6 +264,7 @@ async def test_respond_when_reply_is_True(self, message):
             channel=456,
             content="test content",
             embed=embed,
+            embeds=embeds,
             attachment=attachment,
             attachments=attachments,
             nonce="nonce",
@@ -266,6 +276,27 @@ async def test_respond_when_reply_is_True(self, message):
             mentions_reply=True,
         )
 
+    async def test_respond_when_reply_is_False(self, message):
+        message.app = mock.AsyncMock()
+        message.id = 123
+        message.channel_id = 456
+        await message.respond(reply=False)
+        message.app.rest.create_message.assert_awaited_once_with(
+            channel=456,
+            content=undefined.UNDEFINED,
+            embed=undefined.UNDEFINED,
+            embeds=undefined.UNDEFINED,
+            attachment=undefined.UNDEFINED,
+            attachments=undefined.UNDEFINED,
+            nonce=undefined.UNDEFINED,
+            tts=undefined.UNDEFINED,
+            reply=undefined.UNDEFINED,
+            mentions_everyone=undefined.UNDEFINED,
+            user_mentions=undefined.UNDEFINED,
+            role_mentions=undefined.UNDEFINED,
+            mentions_reply=undefined.UNDEFINED,
+        )
+
     async def test_delete(self, message):
         message.app = mock.AsyncMock()
         message.id = 123
diff --git a/tests/hikari/test_permissions.py b/tests/hikari/test_permissions.py
index ef09b994..409a1b61 100644
--- a/tests/hikari/test_permissions.py
+++ b/tests/hikari/test_permissions.py
@@ -19,3 +19,12 @@
 # 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.
+from hikari import permissions
+
+
+class TestPermissions:
+    def test_all_permissions(self):
+        all_perms = permissions.Permissions.all_permissions()
+
+        assert isinstance(all_perms, permissions.Permissions)
+        assert all_perms == 128849018879
diff --git a/tests/hikari/test_users.py b/tests/hikari/test_users.py
index f04e0810..4ea713c6 100644
--- a/tests/hikari/test_users.py
+++ b/tests/hikari/test_users.py
@@ -23,6 +23,7 @@
 import pytest
 
 from hikari import snowflakes
+from hikari import traits
 from hikari import undefined
 from hikari import urls
 from hikari import users
@@ -45,25 +46,125 @@ async def test_fetch_self(self, obj):
         obj.app.rest.fetch_user.assert_awaited_once_with(user=123)
 
     @pytest.mark.asyncio()
-    async def test_send(self, obj):
+    async def test_send_uses_cached_id(self, obj):
+        obj.id = 4123123
         embed = object()
+        embeds = [object()]
         attachment = object()
         attachments = [object(), object()]
         user_mentions = [object(), object()]
         role_mentions = [object(), object()]
-        mock_channel = mock.Mock(id=456)
-        mock_message = object()
         reply = object()
         mentions_reply = object()
 
-        obj.app = mock.Mock()
-        obj.fetch_dm_channel = mock.AsyncMock(return_value=mock_channel)
-        obj.app.rest.create_message = mock.AsyncMock(return_value=mock_message)
-        obj._dm_channel = None
+        obj.app = mock.Mock(spec=traits.CacheAware, rest=mock.AsyncMock())
+        obj.fetch_dm_channel = mock.AsyncMock()
+
+        returned = await obj.send(
+            content="test",
+            embed=embed,
+            embeds=embeds,
+            attachment=attachment,
+            attachments=attachments,
+            nonce="nonce",
+            tts=True,
+            reply=reply,
+            mentions_everyone=False,
+            user_mentions=user_mentions,
+            role_mentions=role_mentions,
+            mentions_reply=mentions_reply,
+        )
+
+        assert returned is obj.app.rest.create_message.return_value
+
+        obj.app.cache.get_dm_channel_id.assert_called_once_with(4123123)
+        obj.fetch_dm_channel.assert_not_called()
+        obj.app.rest.create_message.assert_awaited_once_with(
+            channel=obj.app.cache.get_dm_channel_id.return_value,
+            content="test",
+            embed=embed,
+            embeds=embeds,
+            attachment=attachment,
+            attachments=attachments,
+            nonce="nonce",
+            tts=True,
+            mentions_everyone=False,
+            reply=reply,
+            user_mentions=user_mentions,
+            role_mentions=role_mentions,
+            mentions_reply=mentions_reply,
+        )
+
+    @pytest.mark.asyncio()
+    async def test_send_when_not_cached(self, obj):
+        obj.id = 522234
+        embed = object()
+        embeds = [object(), object()]
+        attachment = object()
+        attachments = [object(), object()]
+        user_mentions = [object(), object()]
+        role_mentions = [object(), object()]
+        reply = object()
+        mentions_reply = object()
+
+        obj.app = mock.Mock(spec=traits.CacheAware, rest=mock.AsyncMock())
+        obj.app.cache.get_dm_channel_id = mock.Mock(return_value=None)
+        obj.fetch_dm_channel = mock.AsyncMock()
+
+        returned = await obj.send(
+            content="test",
+            embed=embed,
+            embeds=embeds,
+            attachment=attachment,
+            attachments=attachments,
+            nonce="nonce",
+            tts=True,
+            reply=reply,
+            mentions_everyone=False,
+            user_mentions=user_mentions,
+            role_mentions=role_mentions,
+            mentions_reply=mentions_reply,
+        )
+
+        assert returned is obj.app.rest.create_message.return_value
+
+        obj.app.cache.get_dm_channel_id.assert_called_once_with(522234)
+        obj.fetch_dm_channel.assert_awaited_once()
+        obj.app.rest.create_message.assert_awaited_once_with(
+            channel=obj.fetch_dm_channel.return_value.id,
+            content="test",
+            embed=embed,
+            embeds=embeds,
+            attachment=attachment,
+            attachments=attachments,
+            nonce="nonce",
+            tts=True,
+            mentions_everyone=False,
+            reply=reply,
+            user_mentions=user_mentions,
+            role_mentions=role_mentions,
+            mentions_reply=mentions_reply,
+        )
+
+    @pytest.mark.asyncio()
+    async def test_send_when_not_cache_aware(self, obj):
+        obj.id = 522234
+        embed = object()
+        embeds = [object(), object()]
+        attachment = object()
+        attachments = [object(), object()]
+        user_mentions = [object(), object()]
+        role_mentions = [object(), object()]
+        reply = object()
+        mentions_reply = object()
+
+        obj.app = mock.Mock(spec=traits.RESTAware, rest=mock.AsyncMock())
+        obj.fetch_dm_channel = mock.AsyncMock()
 
         returned = await obj.send(
             content="test",
             embed=embed,
+            embeds=embeds,
             attachment=attachment,
             attachments=attachments,
             nonce="nonce",
@@ -75,13 +176,14 @@ async def test_send(self, obj):
             mentions_reply=mentions_reply,
         )
 
-        assert returned is mock_message
+        assert returned is obj.app.rest.create_message.return_value
 
-        obj.fetch_dm_channel.assert_awaited_once_with()
+        obj.fetch_dm_channel.assert_awaited_once()
         obj.app.rest.create_message.assert_awaited_once_with(
-            channel=456,
+            channel=obj.fetch_dm_channel.return_value.id,
             content="test",
             embed=embed,
+            embeds=embeds,
             attachment=attachment,
             attachments=attachments,
             nonce="nonce",
@@ -93,6 +195,16 @@ async def test_send(self, obj):
             mentions_reply=mentions_reply,
         )
 
+    @pytest.mark.asyncio()
+    async def test_fetch_dm_channel(self, obj):
+        obj.id = 123
+        obj.app = mock.Mock()
+        obj.app.rest.create_dm_channel = mock.AsyncMock()
+
+        assert await obj.fetch_dm_channel() == obj.app.rest.create_dm_channel.return_value
+
+        obj.app.rest.create_dm_channel.assert_awaited_once_with(123)
+
 
 class TestUser:
     @pytest.fixture()
@@ -202,30 +314,6 @@ def test_str_operator_when_partial(self, obj):
     def test_mention_property(self, obj):
         assert obj.mention == "<@123>"
 
-    @pytest.mark.asyncio()
-    async def test_fetch_dm_channel_when_not_cached(self, obj):
-        mock_channel = mock.Mock(id=456)
-
-        obj.id = 123
-        obj.app = mock.Mock()
-        obj.app.rest.create_dm_channel = mock.AsyncMock(return_value=mock_channel)
-        obj._dm_channel = None
-
-        assert await obj.fetch_dm_channel() == mock_channel
-
-        assert obj._dm_channel == mock_channel
-        obj.app.rest.create_dm_channel.assert_awaited_once_with(123)
-
-    @pytest.mark.asyncio()
-    async def test_fetch_dm_channel_when_cached(self, obj):
-        mock_channel = mock.Mock(id=456)
-        obj._dm_channel = mock_channel
-
-        assert await obj.fetch_dm_channel() == mock_channel
-
-        assert obj._dm_channel == mock_channel
-        obj.app.rest.create_dm_channel.assert_not_called()
-
     @pytest.mark.asyncio()
     async def test_fetch_self(self, obj):
         user = object()