Repository: hikari-py/hikari
Pull request #1683 (2023-08-09 08:14:23)

Add custom status support

davfsa: `name` is still required to be passed for custom statuses for bots, even if its not used, which is not the best DX.

Might be worth having some syntactical sugar to default `name` to the value of `state` if its passed.

Thoughts?

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 67a25b3f..ce355fde 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,7 +23,7 @@ jobs:
 
     steps:
       - name: Checkout repository
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Setup python
         uses: actions/setup-python@v4
@@ -66,7 +66,7 @@ jobs:
 
     steps:
       - name: Checkout repository
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Setup python
         uses: actions/setup-python@v4
@@ -97,7 +97,7 @@ jobs:
 
     steps:
       - name: Checkout repository
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Setup python
         uses: actions/setup-python@v4
@@ -154,7 +154,7 @@ jobs:
 
     steps:
       - name: Checkout repository
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Setup python
         uses: actions/setup-python@v4
@@ -171,12 +171,12 @@ jobs:
 
     steps:
       - name: Checkout repository
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Setup python
         uses: actions/setup-python@v4
         with:
-          python-version: 3.8
+          python-version: 3.9
 
       - name: Setup node
         uses: actions/setup-node@v3
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index edda10ab..19e06cba 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -19,7 +19,7 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
 
     - name: Initialize CodeQL
       uses: github/codeql-action/init@v2
diff --git a/.github/workflows/fragments-check.yml b/.github/workflows/fragments-check.yml
index cececdc2..f3866844 100644
--- a/.github/workflows/fragments-check.yml
+++ b/.github/workflows/fragments-check.yml
@@ -13,7 +13,7 @@ jobs:
 
     steps:
       - name: Checkout repository
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           # `towncrier check` runs `git diff --name-only origin/main...`, which
           # needs a non-shallow clone.
diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml
index 99e7bb2b..cf52a341 100644
--- a/.github/workflows/prepare-release.yml
+++ b/.github/workflows/prepare-release.yml
@@ -22,7 +22,7 @@ jobs:
           private_key: ${{ secrets.PRIVATE_KEY }}
 
       - name: Checkout repository
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           token: ${{ steps.generate_token.outputs.token }}
 
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7d84b6f0..8c49d2cf 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,7 +17,7 @@ jobs:
           private_key: ${{ secrets.PRIVATE_KEY }}
 
       - name: Checkout repository
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           token: ${{ steps.generate_token.outputs.token }}
 
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 2d49d784..ef6a217b 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -3,7 +3,7 @@ version: 2
 build:
   os: ubuntu-22.04
   tools:
-    python: "3.8"
+    python: "3.9"
 
 sphinx:
   configuration: docs/conf.py
diff --git a/README.md b/README.md
index 88f2070c..1ad27581 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ Hikari provides two different default bot implementations to suit your needs:
 
 ### GatewayBot
 
-A [`GatewayBot`](https://docs.hikari-py.dev/en/stable/reference/hikari/impl/bot/#hikari.impl.bot.GatewayBot)
+A [`GatewayBot`](https://docs.hikari-py.dev/en/stable/reference/hikari/impl/gateway_bot/#hikari.impl.gateway_bot.GatewayBot)
 is one which will connect to Discord through the gateway and receive
 events through there. A simple startup example could be the following:
 
@@ -194,7 +194,7 @@ bot.run(
 
 Many other helpful options exist for you to take advantage of if you wish. Links to the respective docs can be seen
 below:
-- [GatewayBot.run](https://docs.hikari-py.dev/en/stable/reference/hikari/impl/bot/#hikari.impl.bot.GatewayBot.run)
+- [GatewayBot.run](https://docs.hikari-py.dev/en/stable/reference/hikari/impl/gateway_bot/#hikari.impl.gateway_bot.GatewayBot.run)
 - [RESTBot.run](https://docs.hikari-py.dev/en/stable/reference/hikari/impl/rest_bot/#hikari.impl.rest_bot.RESTBot.run)
 
 ---
diff --git a/changes/1679.feature.md b/changes/1679.feature.md
new file mode 100644
index 00000000..4e2839f5
--- /dev/null
+++ b/changes/1679.feature.md
@@ -0,0 +1,1 @@
+Add `CacheSettings.only_my_member` to only cache the bot member.
diff --git a/changes/1683.feature.md b/changes/1683.feature.md
new file mode 100644
index 00000000..0fe12cbb
--- /dev/null
+++ b/changes/1683.feature.md
@@ -0,0 +1,4 @@
+Bots can now utilize `Activity.state`
+- When used with `type` set to `ActivityType.CUSTOM`, it will show as the text for the custom status.
+  Syntactic sugar also exists to support simply using `name` instead of `state`.
+- Can be used with other activity types to provide additional information on the activity.
diff --git a/changes/1702.bugfix.md b/changes/1702.bugfix.md
new file mode 100644
index 00000000..a686079c
--- /dev/null
+++ b/changes/1702.bugfix.md
@@ -0,0 +1,1 @@
+Fix attachments not being removed in message edits when `attachment` or `attachments` is `None`
diff --git a/changes/1705.feature.md b/changes/1705.feature.md
new file mode 100644
index 00000000..2d52cdd0
--- /dev/null
+++ b/changes/1705.feature.md
@@ -0,0 +1,1 @@
+Add missing Audit Log event types to `AuditLogEventType`
diff --git a/changes/1712.feature.md b/changes/1712.feature.md
new file mode 100644
index 00000000..ca623497
--- /dev/null
+++ b/changes/1712.feature.md
@@ -0,0 +1,1 @@
+Add `approximate_guild_count` field to own `Application`
diff --git a/dev-requirements/build.txt b/dev-requirements/build.txt
index f8138410..5e743662 100644
--- a/dev-requirements/build.txt
+++ b/dev-requirements/build.txt
@@ -1,2 +1,2 @@
 setuptools>=65.2.0
-wheel==0.41.1
+wheel==0.41.2
diff --git a/dev-requirements/coverage.txt b/dev-requirements/coverage.txt
index 856380d3..15c3f67b 100644
--- a/dev-requirements/coverage.txt
+++ b/dev-requirements/coverage.txt
@@ -1,1 +1,1 @@
-coverage[toml]==7.2.7
+coverage[toml]==7.3.1
diff --git a/dev-requirements/mypy.txt b/dev-requirements/mypy.txt
index 49dc1503..d0c68f7f 100644
--- a/dev-requirements/mypy.txt
+++ b/dev-requirements/mypy.txt
@@ -1,1 +1,1 @@
-mypy==1.4.1
+mypy==1.5.1
diff --git a/dev-requirements/pyright.txt b/dev-requirements/pyright.txt
index f42b8976..554246fc 100644
--- a/dev-requirements/pyright.txt
+++ b/dev-requirements/pyright.txt
@@ -1,1 +1,1 @@
-pyright==1.1.320
+pyright==1.1.326
diff --git a/dev-requirements/sphinx.txt b/dev-requirements/sphinx.txt
index 4f33125f..b5158aec 100644
--- a/dev-requirements/sphinx.txt
+++ b/dev-requirements/sphinx.txt
@@ -1,7 +1,7 @@
-sphinx==7.1.2
+sphinx==7.2.5
 
 # Theme
-furo==2023.7.26
+furo==2023.8.19
 
 # Autodocumentation
 sphinx-autoapi==2.1.1
diff --git a/hikari/__init__.py b/hikari/__init__.py
index 45a0066f..c4ae3280 100644
--- a/hikari/__init__.py
+++ b/hikari/__init__.py
@@ -22,7 +22,7 @@
 # SOFTWARE.
 """A sane Python framework for writing modern Discord bots.
 
-To get started, you will want to initialize an instance of `hikari.impl.bot.GatewayBot`
+To get started, you will want to initialize an instance of `hikari.impl.gateway_bot.GatewayBot`
 for writing a gateway based bot, `hikari.impl.rest_bot.RESTBot` for a REST based bot,
 or `hikari.impl.rest.RESTApp` if you only need to use the REST API.
 """
diff --git a/hikari/api/config.py b/hikari/api/config.py
index ef6b05b7..8907783b 100644
--- a/hikari/api/config.py
+++ b/hikari/api/config.py
@@ -77,7 +77,7 @@ class CacheComponents(enums.Flag):
     """Enables the guild stickers cache."""
 
     GUILD_THREADS = 1 << 12
-    """Enabled the guild threads cache."""
+    """Enables the guild threads cache."""
 
     ALL = (
         GUILDS
@@ -199,3 +199,14 @@ class CacheSettings(abc.ABC):
     @abc.abstractmethod
     def components(self) -> CacheComponents:
         """Cache components to use."""
+
+    @property
+    @abc.abstractmethod
+    def only_my_member(self) -> bool:
+        """Reduce the members cache to only the bot itself.
+
+        Useful when only the bot member is required (eg. permission checks).
+        This will have no effect if the members cache is not enabled.
+
+        Defaults to `False`.
+        """
diff --git a/hikari/applications.py b/hikari/applications.py
index e6472e3f..c6d62272 100644
--- a/hikari/applications.py
+++ b/hikari/applications.py
@@ -632,6 +632,9 @@ class Application(guilds.PartialApplication):
     install_parameters: typing.Optional[ApplicationInstallParameters] = attrs.field(eq=False, hash=False, repr=False)
     """Settings for the application's default in-app authorization link, if enabled."""
 
+    approximate_guild_count: int = attrs.field(eq=False, hash=False, repr=False)
+    """The approximate number of guilds this application is part of."""
+
     @property
     def cover_image_url(self) -> typing.Optional[files.URL]:
         """Rich presence cover image URL for this application, if set."""
diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py
index a9c09da8..6b933a50 100644
--- a/hikari/audit_logs.py
+++ b/hikari/audit_logs.py
@@ -334,6 +334,11 @@ class AuditLogEventType(int, enums.Enum):
     GUILD_SCHEDULED_EVENT_UPDATE = 101
     GUILD_SCHEDULED_EVENT_DELETE = 102
     APPLICATION_COMMAND_PERMISSION_UPDATE = 121
+    THREAD_CREATE = 110
+    THREAD_UPDATE = 111
+    THREAD_DELETE = 112
+    CREATOR_MONETIZATION_REQUEST_CREATED = 150
+    CREATOR_MONETIZATION_TERMS_ACCEPTED = 151
 
 
 @attrs.define(hash=False, kw_only=True, weakref_slot=False)
diff --git a/hikari/impl/config.py b/hikari/impl/config.py
index 4f0916f4..c7fc36b3 100644
--- a/hikari/impl/config.py
+++ b/hikari/impl/config.py
@@ -29,6 +29,7 @@
     "ProxySettings",
     "HTTPTimeoutSettings",
     "HTTPSettings",
+    "CacheComponents",
     "CacheSettings",
 )
 
@@ -292,6 +293,10 @@ class that will **NOT** enforce SSL verification. This is then stored
     """
 
 
+# Re-export
+CacheComponents = config.CacheComponents
+
+
 @attrs_extensions.with_copy
 @attrs.define(kw_only=True, weakref_slot=False)
 class CacheSettings(config.CacheSettings):
@@ -320,3 +325,12 @@ class CacheSettings(config.CacheSettings):
 
     Defaults to `50`.
     """
+
+    only_my_member: bool = attrs.field(default=False)
+    """Reduce the members cache to only the bot itself.
+
+    Useful when only the bot member is required (eg. permission checks).
+    This will have no effect if the members cache is not enabled.
+
+    Defaults to `False`.
+    """
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index aaa8fbed..b5358e2a 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -651,6 +651,7 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati
             custom_install_url=payload.get("custom_install_url"),
             tags=payload.get("tags") or [],
             install_parameters=install_parameters,
+            approximate_guild_count=payload["approximate_guild_count"],
         )
 
     def deserialize_authorization_information(
diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py
index ef1c6590..e9ec111a 100644
--- a/hikari/impl/event_manager.py
+++ b/hikari/impl/event_manager.py
@@ -343,8 +343,12 @@ async def on_guild_create(  # noqa: C901, CFQ001 - Function too complex and too
             if members:
                 # TODO: do we really want to invalidate these all after an outage.
                 self._cache.clear_members_for_guild(guild_id)
-                for member in members.values():
-                    self._cache.set_member(member)
+                if not self._cache.settings.only_my_member:
+                    for member in members.values():
+                        self._cache.set_member(member)
+                else:
+                    my_member = members[shard.get_user_id()]
+                    self._cache.set_member(my_member)
 
             if presences:
                 self._cache.clear_presences_for_guild(guild_id)
@@ -361,17 +365,34 @@ async def on_guild_create(  # noqa: C901, CFQ001 - Function too complex and too
                 for thread in threads.values():
                     self._cache.set_thread(thread)
 
+        # We only want to chunk if we are allowed and need to:
+        #   Allowed?
+        #       All the following must be true:
+        #           1. `auto_chunk_members` is true (the user wants us to).
+        #           2. We have the necessary intents (`GUILD_MEMBERS`).
+        #           3. The guild is marked as "large" or we do not have `GUILD_PRESENCES` intent
+        #              Discord will only send every other member objects on the `GUILD_CREATE`
+        #              payload if presence intents are also declared, so if this isn't the case then we also
+        #              want to chunk small guilds.
+        #
+        #   Need to?
+        #       One of the following must be true:
+        #           1. We have a cache, and it requires it (it is enabled for `MEMBERS`), but we are
+        #              not limited to only our own member (which is included in the `GUILD_CREATE` payload).
+        #           2. The user is waiting for the member chunks (there is an event listener for it).
         presences_declared = self._intents & intents_.Intents.GUILD_PRESENCES
 
-        # When intents are enabled Discord will only send other member objects on the guild create
-        # payload if presence intents are also declared, so if this isn't the case then we also want
-        # to chunk small guilds.
         if (
             self._auto_chunk_members
             and self._intents & intents_.Intents.GUILD_MEMBERS
             and (payload.get("large") or not presences_declared)
             and (
-                self._cache_enabled_for(config.CacheComponents.MEMBERS)
+                (
+                    self._cache
+                    and self._cache_enabled_for(config.CacheComponents.MEMBERS)
+                    and not self._cache.settings.only_my_member
+                )
+                # This call is a bit expensive, so best to do it last
                 or self._enabled_for_event(shard_events.MemberChunkEvent)
             )
         ):
diff --git a/hikari/impl/gateway_bot.py b/hikari/impl/gateway_bot.py
index 0d3f8843..debe57bb 100644
--- a/hikari/impl/gateway_bot.py
+++ b/hikari/impl/gateway_bot.py
@@ -78,19 +78,12 @@
 def _validate_activity(activity: undefined.UndefinedNoneOr[presences.Activity]) -> None:
     # This seems to cause confusion for a lot of people, so lets add some warnings into the mix.
 
-    if activity is undefined.UNDEFINED or activity is None:
+    if not activity:
         return
 
     # If you ever change where this is called from, make sure to check the stacklevels are correct
     # or the code preview in the warning will be wrong...
-    if activity.type is presences.ActivityType.CUSTOM:
-        warnings.warn(
-            "The CUSTOM activity type is not supported by bots at the time of writing, and may therefore not have "
-            "any effect if used.",
-            category=errors.HikariWarning,
-            stacklevel=3,
-        )
-    elif activity.type is presences.ActivityType.STREAMING and activity.url is None:
+    if activity.type is presences.ActivityType.STREAMING and activity.url is None:
         warnings.warn(
             "The STREAMING activity type requires a 'url' parameter pointing to a valid Twitch or YouTube video "
             "URL to be specified on the activity for the presence update to have any effect.",
@@ -183,15 +176,22 @@ class GatewayBot(traits.GatewayBotAware):
         Defaults to `True`. If `False`, then no member chunks
         will be requested automatically, even if there are reasons to do so.
 
-        All following statements must be true to automatically request chunks:
-
-        1. `auto_chunk_members` is `True`.
-        2. The members intent is enabled.
-        3. The server is marked as "large" or the presences intent is not enabled
-           (since Discord only sends other members when presences are declared,
-           we should also chunk small guilds if the presences are not declared).
-        4. The members cache is enabled or there are listeners for the
-           `MemberChunkEvent`.
+        We only want to chunk if we are allowed and need to:
+
+        - Allowed?
+            All the following must be true:
+                1. `auto_chunk_members` is true (the user wants us to).
+                2. We have the necessary intents (`GUILD_MEMBERS`).
+                3. The guild is marked as "large" or we do not have `GUILD_PRESENCES` intent
+                   Discord will only send every other member objects on the `GUILD_CREATE`
+                   payload if presence intents are also declared, so if this isn't the case then we also
+                   want to chunk small guilds.
+
+        - Needed?
+            One of the following must be true:
+                1. We have a cache, and it requires it (it is enabled for `MEMBERS`), but we are
+                   not limited to only our own member (which is included in the `GUILD_CREATE` payload).
+                2. The user is waiting for the member chunks (there is an event listener for it).
     logs : typing.Union[None, str, int, typing.Dict[str, typing.Any], os.PathLike]
         The flavour to set the logging to.
 
@@ -563,11 +563,11 @@ async def on_everyone_mentioned(event):
 
         See Also
         --------
-        Listen : `hikari.impl.bot.GatewayBot.listen`.
-        Stream : `hikari.impl.bot.GatewayBot.stream`.
-        Subscribe : `hikari.impl.bot.GatewayBot.subscribe`.
-        Unsubscribe : `hikari.impl.bot.GatewayBot.unsubscribe`.
-        Wait_for : `hikari.impl.bot.GatewayBot.wait_for`.
+        Listen : `hikari.impl.gateway_bot.GatewayBot.listen`.
+        Stream : `hikari.impl.gateway_bot.GatewayBot.stream`.
+        Subscribe : `hikari.impl.gateway_bot.GatewayBot.subscribe`.
+        Unsubscribe : `hikari.impl.gateway_bot.GatewayBot.unsubscribe`.
+        Wait_for : `hikari.impl.gateway_bot.GatewayBot.wait_for`.
         """
         return self._event_manager.dispatch(event)
 
@@ -626,11 +626,11 @@ def listen(
 
         See Also
         --------
-        Dispatch : `hikari.impl.bot.GatewayBot.dispatch`.
-        Stream : `hikari.impl.bot.GatewayBot.stream`.
-        Subscribe : `hikari.impl.bot.GatewayBot.subscribe`.
-        Unsubscribe : `hikari.impl.bot.GatewayBot.unsubscribe`.
-        Wait_for : `hikari.impl.bot.GatewayBot.wait_for`.
+        Dispatch : `hikari.impl.gateway_bot.GatewayBot.dispatch`.
+        Stream : `hikari.impl.gateway_bot.GatewayBot.stream`.
+        Subscribe : `hikari.impl.gateway_bot.GatewayBot.subscribe`.
+        Unsubscribe : `hikari.impl.gateway_bot.GatewayBot.unsubscribe`.
+        Wait_for : `hikari.impl.gateway_bot.GatewayBot.wait_for`.
         """
         return self._event_manager.listen(*event_types)
 
@@ -1056,11 +1056,11 @@ def stream(
 
         See Also
         --------
-        Dispatch : `hikari.impl.bot.GatewayBot.dispatch`.
-        Listen : `hikari.impl.bot.GatewayBot.listen`.
-        Subscribe : `hikari.impl.bot.GatewayBot.subscribe`.
-        Unsubscribe : `hikari.impl.bot.GatewayBot.unsubscribe`.
-        Wait_for : `hikari.impl.bot.GatewayBot.wait_for`.
+        Dispatch : `hikari.impl.gateway_bot.GatewayBot.dispatch`.
+        Listen : `hikari.impl.gateway_bot.GatewayBot.listen`.
+        Subscribe : `hikari.impl.gateway_bot.GatewayBot.subscribe`.
+        Unsubscribe : `hikari.impl.gateway_bot.GatewayBot.unsubscribe`.
+        Wait_for : `hikari.impl.gateway_bot.GatewayBot.wait_for`.
         """
         self._check_if_alive()
         return self._event_manager.stream(event_type, timeout=timeout, limit=limit)
@@ -1099,11 +1099,11 @@ async def on_message(event):
 
         See Also
         --------
-        Dispatch : `hikari.impl.bot.GatewayBot.dispatch`.
-        Listen : `hikari.impl.bot.GatewayBot.listen`.
-        Stream : `hikari.impl.bot.GatewayBot.stream`.
-        Unsubscribe : `hikari.impl.bot.GatewayBot.unsubscribe`.
-        Wait_for : `hikari.impl.bot.GatewayBot.wait_for`.
+        Dispatch : `hikari.impl.gateway_bot.GatewayBot.dispatch`.
+        Listen : `hikari.impl.gateway_bot.GatewayBot.listen`.
+        Stream : `hikari.impl.gateway_bot.GatewayBot.stream`.
+        Unsubscribe : `hikari.impl.gateway_bot.GatewayBot.unsubscribe`.
+        Wait_for : `hikari.impl.gateway_bot.GatewayBot.wait_for`.
         """
         self._event_manager.subscribe(event_type, callback)
 
@@ -1139,11 +1139,11 @@ async def on_message(event):
 
         See Also
         --------
-        Dispatch : `hikari.impl.bot.GatewayBot.dispatch`.
-        Listen : `hikari.impl.bot.GatewayBot.listen`.
-        Stream : `hikari.impl.bot.GatewayBot.stream`.
-        Subscribe : `hikari.impl.bot.GatewayBot.subscribe`.
-        Wait_for : `hikari.impl.bot.GatewayBot.wait_for`.
+        Dispatch : `hikari.impl.gateway_bot.GatewayBot.dispatch`.
+        Listen : `hikari.impl.gateway_bot.GatewayBot.listen`.
+        Stream : `hikari.impl.gateway_bot.GatewayBot.stream`.
+        Subscribe : `hikari.impl.gateway_bot.GatewayBot.subscribe`.
+        Wait_for : `hikari.impl.gateway_bot.GatewayBot.wait_for`.
         """
         self._event_manager.unsubscribe(event_type, callback)
 
@@ -1190,11 +1190,11 @@ async def wait_for(
 
         See Also
         --------
-        Dispatch : `hikari.impl.bot.GatewayBot.dispatch`.
-        Listen : `hikari.impl.bot.GatewayBot.listen`.
-        Stream : `hikari.impl.bot.GatewayBot.stream`.
-        Subscribe : `hikari.impl.bot.GatewayBot.subscribe`.
-        Unsubscribe : `hikari.impl.bot.GatewayBot.unsubscribe`.
+        Dispatch : `hikari.impl.gateway_bot.GatewayBot.dispatch`.
+        Listen : `hikari.impl.gateway_bot.GatewayBot.listen`.
+        Stream : `hikari.impl.gateway_bot.GatewayBot.stream`.
+        Subscribe : `hikari.impl.gateway_bot.GatewayBot.subscribe`.
+        Unsubscribe : `hikari.impl.gateway_bot.GatewayBot.unsubscribe`.
         """
         self._check_if_alive()
         return await self._event_manager.wait_for(event_type, timeout=timeout, predicate=predicate)
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index 024704ff..fbf8ae43 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -1484,7 +1484,7 @@ def _build_message_payload(  # noqa: C901- Function too complex
             body.put("attachments", attachments_payload)
 
         elif attachment is None or attachments is None:
-            body.put("attachments", None)
+            body.put("attachments", [])
 
         return body, form_builder
 
diff --git a/hikari/impl/shard.py b/hikari/impl/shard.py
index 607eb831..5738941c 100644
--- a/hikari/impl/shard.py
+++ b/hikari/impl/shard.py
@@ -113,6 +113,8 @@
         errors.ShardCloseCode.RATE_LIMITED,
     )
 )
+# Default value used by the client
+_CUSTOM_STATUS_NAME = "Custom Status"
 
 
 def _log_filterer(token: str) -> typing.Callable[[str], str]:
@@ -352,7 +354,17 @@ def _serialize_activity(activity: typing.Optional[presences.Activity]) -> data_b
     if activity is None:
         return None
 
-    return {"name": activity.name, "type": int(activity.type), "url": activity.url}
+    # Syntactic sugar, treat `name` as state if using `CUSTOM` and `state` is not passed.
+    state: typing.Optional[str]
+    if activity.type is presences.ActivityType.CUSTOM and activity.name and not activity.state:
+        name = _CUSTOM_STATUS_NAME
+        state = activity.name
+    else:
+        name = activity.name
+        state = activity.state
+
+    payload = {"name": name, "state": state, "type": int(activity.type), "url": activity.url}
+    return payload
 
 
 class GatewayShardImpl(shard.GatewayShard):
diff --git a/hikari/presences.py b/hikari/presences.py
index 3f07271b..6d879bfc 100644
--- a/hikari/presences.py
+++ b/hikari/presences.py
@@ -81,13 +81,10 @@ class ActivityType(int, enums.Enum):
     """Shows up as `Watching <name>`."""
 
     CUSTOM = 4
-    """A custom status.
-
-    To set an emoji with the status, place a unicode emoji or Discord emoji
-    (`:smiley:`) as the first part of the status activity name.
+    """Shows up as `<emoji> <name>`.
 
     .. warning::
-        Bots **DO NOT** support setting custom statuses.
+        As of the time of writing, emoji cannot be used by bot accounts.
     """
 
     COMPETING = 5
@@ -306,8 +303,18 @@ class Activity:
     name: str = attrs.field()
     """The activity name."""
 
+    state: typing.Optional[str] = attrs.field(default=None)
+    """The activities state, if set.
+
+    This field can be use to set a custom status or provide more information
+    on the activity.
+    """
+
     url: typing.Optional[str] = attrs.field(default=None, repr=False)
-    """The activity URL. Only valid for `STREAMING` activities."""
+    """The activity URL, if set.
+
+    Only valid for `STREAMING` activities.
+    """
 
     type: typing.Union[ActivityType, int] = attrs.field(converter=ActivityType, default=ActivityType.PLAYING)
     """The activity type."""
@@ -332,9 +339,6 @@ class RichActivity(Activity):
     details: typing.Optional[str] = attrs.field(repr=False)
     """The text that describes what the activity's target is doing, if set."""
 
-    state: typing.Optional[str] = attrs.field(repr=False)
-    """The current status of this activity's target, if set."""
-
     emoji: typing.Optional[emojis_.Emoji] = attrs.field(repr=False)
     """The emoji of this activity, if it is a custom status and set."""
 
diff --git a/hikari/users.py b/hikari/users.py
index b58e1562..d6e44162 100644
--- a/hikari/users.py
+++ b/hikari/users.py
@@ -168,7 +168,7 @@ def accent_colour(self) -> undefined.UndefinedNoneOr[colors.Color]:
     def discriminator(self) -> undefined.UndefinedOr[str]:
         """Discriminator for the user.
 
-        .. deprecation:: 2.0.0.dev120
+        .. deprecated:: 2.0.0.dev120
             Discriminators are deprecated and being replaced with "0" by Discord
             during username migration. This field will be removed after migration is complete.
             Learn more here: https://dis.gd/usernames
@@ -514,7 +514,7 @@ def display_avatar_url(self) -> files.URL:
     def discriminator(self) -> str:
         """Discriminator for the user.
 
-        .. deprecation:: 2.0.0.dev120
+        .. deprecated:: 2.0.0.dev120
             Discriminators are deprecated and being replaced with "0" by Discord
             during username migration. This field will be removed after migration is complete.
             Learn more here: https://dis.gd/usernames
@@ -665,7 +665,7 @@ class PartialUserImpl(PartialUser):
     discriminator: undefined.UndefinedOr[str] = attrs.field(eq=False, hash=False, repr=True)
     """Four-digit discriminator for the user if unmigrated.
 
-    .. deprecation:: 2.0.0.dev120
+    .. deprecated:: 2.0.0.dev120
         Discriminators are deprecated and being replaced with "0" by Discord
         during username migration. This field will be removed after migration is complete.
         Learn more here: https://dis.gd/usernames
@@ -726,7 +726,7 @@ class UserImpl(PartialUserImpl, User):
     discriminator: str = attrs.field(eq=False, hash=False, repr=True)
     """The user's discriminator.
 
-    .. deprecation:: 2.0.0.dev120
+    .. deprecated:: 2.0.0.dev120
         Discriminators are deprecated and being replaced with "0" by Discord
         during username migration. This field will be removed after migration is complete.
         Learn more here: https://dis.gd/usernames


Test patch:
diff --git a/dev-requirements/pytest.txt b/dev-requirements/pytest.txt
index c0fa0228..eca23ade 100644
--- a/dev-requirements/pytest.txt
+++ b/dev-requirements/pytest.txt
@@ -1,11 +1,11 @@
 # mock in the stdlib changes between versions of Python, so are not consistent in behaviour like the backport is
 mock==5.1.0
 
-pytest==7.4.0
+pytest==7.4.2
 pytest-asyncio==0.21.1
 pytest-cov==4.1.0
-pytest-randomly==3.13.0
+pytest-randomly==3.15.0
 
-async-timeout==4.0.2  # Used for timeouts in some test cases.
+async-timeout==4.0.3  # Used for timeouts in some test cases.
 
 -r coverage.txt  # Consistent coverage version
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index c7a2f34a..47fd133c 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -975,6 +975,7 @@ def application_payload(self, owner_payload, user_payload):
             "custom_install_url": "https://dontinstallme.com",
             "tags": ["i", "like", "hikari"],
             "install_params": {"scopes": ["bot", "applications.commands"], "permissions": 8},
+            "approximate_guild_count": 10000,
         }
 
     def test_deserialize_application(
@@ -1001,6 +1002,7 @@ def test_deserialize_application(
         assert application.custom_install_url == "https://dontinstallme.com"
         assert application.tags == ["i", "like", "hikari"]
         assert application.icon_hash == "iwiwiwiwiw"
+        assert application.approximate_guild_count == 10000
         # Install Parameters
         assert application.install_parameters.scopes == [
             application_models.OAuth2Scope.BOT,
@@ -1037,6 +1039,7 @@ def test_deserialize_application_with_unset_fields(self, entity_factory_impl, mo
                 "verify_key": "1232313223",
                 "flags": 0,
                 "owner": owner_payload,
+                "approximate_guild_count": 10000,
             }
         )
 
@@ -1061,6 +1064,7 @@ def test_deserialize_application_with_null_fields(self, entity_factory_impl, moc
                 "bot_require_code_grant": False,
                 "verify_key": "1232313223",
                 "flags": 0,
+                "approximate_guild_count": 10000,
             }
         )
 
diff --git a/tests/hikari/impl/test_event_manager.py b/tests/hikari/impl/test_event_manager.py
index 5ea1c46c..3068c416 100644
--- a/tests/hikari/impl/test_event_manager.py
+++ b/tests/hikari/impl/test_event_manager.py
@@ -551,15 +551,19 @@ async def test_on_guild_create_when_not_dispatching_and_not_caching(
 
         event_manager_impl.dispatch.assert_not_called()
 
-    @pytest.mark.parametrize("include_unavailable", [True, False])
+    @pytest.mark.parametrize(
+        ("include_unavailable", "only_my_member"), [(True, True), (True, False), (False, True), (False, False)]
+    )
     @pytest.mark.asyncio()
     async def test_on_guild_create_when_not_dispatching_and_caching(
-        self, event_manager_impl, shard, event_factory, entity_factory, include_unavailable
+        self, event_manager_impl, shard, event_factory, entity_factory, include_unavailable, only_my_member
     ):
         payload = {"unavailable": False} if include_unavailable else {}
         event_manager_impl._intents = intents.Intents.NONE
         event_manager_impl._cache_enabled_for = mock.Mock(return_value=True)
         event_manager_impl._enabled_for_event = mock.Mock(return_value=False)
+        event_manager_impl._cache.settings.only_my_member = only_my_member
+        shard.get_user_id.return_value = 1
         gateway_guild = entity_factory.deserialize_gateway_guild.return_value
         gateway_guild.channels.return_value = {1: "channel1", 2: "channel2"}
         gateway_guild.emojis.return_value = {1: "emoji1", 2: "emoji2"}
@@ -595,13 +599,17 @@ async def test_on_guild_create_when_not_dispatching_and_caching(
         event_manager_impl._cache.clear_roles_for_guild.assert_called_once_with(gateway_guild.id)
         event_manager_impl._cache.set_role.assert_has_calls([mock.call("role1"), mock.call("role2")])
         event_manager_impl._cache.clear_members_for_guild.assert_called_once_with(gateway_guild.id)
-        event_manager_impl._cache.set_member.assert_has_calls([mock.call("member1"), mock.call("member2")])
+        if only_my_member:
+            event_manager_impl._cache.set_member.assert_called_once_with("member1")
+            shard.get_user_id.assert_has_calls([mock.call(), mock.call()])
+        else:
+            event_manager_impl._cache.set_member.assert_has_calls([mock.call("member1"), mock.call("member2")])
+            shard.get_user_id.assert_called_once_with()
         event_manager_impl._cache.clear_presences_for_guild.assert_called_once_with(gateway_guild.id)
         event_manager_impl._cache.set_presence.assert_has_calls([mock.call("presence1"), mock.call("presence2")])
         event_manager_impl._cache.clear_voice_states_for_guild.assert_called_once_with(gateway_guild.id)
         event_manager_impl._cache.set_voice_state.assert_has_calls([mock.call("voice1"), mock.call("voice2")])
         request_guild_members.assert_not_called()
-        shard.get_user_id.assert_called_once_with()
 
         event_manager_impl.dispatch.assert_not_called()
 
@@ -633,25 +641,58 @@ async def test_on_guild_create_when_stateless(
         stateless_event_manager_impl.dispatch.assert_not_called()
 
     @pytest.mark.asyncio()
-    async def test_on_guild_create_when_members_declared_and_member_cache_enabled(
-        self, stateless_event_manager_impl, shard, event_factory, entity_factory
+    async def test_on_guild_create_when_members_declared_and_member_cache_enabled_but_only_my_member_not_enabled(
+        self, event_manager_impl, shard, event_factory, entity_factory
     ):
+        def cache_enabled_for_members_only(component):
+            return component == config.CacheComponents.MEMBERS
+
         shard.id = 123
-        stateless_event_manager_impl._intents = intents.Intents.GUILD_MEMBERS
-        stateless_event_manager_impl._cache_enabled_for = mock.Mock(return_value=True)
-        stateless_event_manager_impl._enabled_for_event = mock.Mock(return_value=False)
+        event_manager_impl._cache.settings.only_my_member = False
+        event_manager_impl._intents = intents.Intents.GUILD_MEMBERS
+        event_manager_impl._cache_enabled_for = cache_enabled_for_members_only
+        event_manager_impl._enabled_for_event = mock.Mock(return_value=False)
+        gateway_guild = entity_factory.deserialize_gateway_guild.return_value
+        gateway_guild.id = 456
+        gateway_guild.members.return_value = {1: "member1", 2: "member2"}
         mock_request_guild_members = mock.Mock()
 
         with mock.patch.object(asyncio, "create_task") as create_task:
             with mock.patch.object(event_manager, "_fixed_size_nonce", return_value="abc"):
                 with mock.patch.object(event_manager, "_request_guild_members", new=mock_request_guild_members):
-                    await stateless_event_manager_impl.on_guild_create(shard, {"id": 456, "large": False})
+                    await event_manager_impl.on_guild_create(shard, {"id": 456, "large": False})
 
         mock_request_guild_members.assert_called_once_with(shard, 456, include_presences=False, nonce="123.abc")
         create_task.assert_called_once_with(
             mock_request_guild_members.return_value, name="123:456 guild create members request"
         )
 
+    @pytest.mark.asyncio()
+    async def test_on_guild_create_when_members_declared_and_member_cache_but_only_my_member_enabled(
+        self, event_manager_impl, shard, event_factory, entity_factory
+    ):
+        def cache_enabled_for_members_only(component):
+            return component == config.CacheComponents.MEMBERS
+
+        shard.id = 123
+        shard.get_user_id.return_value = 1
+        event_manager_impl._cache.settings.only_my_member = True
+        event_manager_impl._intents = intents.Intents.GUILD_MEMBERS
+        event_manager_impl._cache_enabled_for = cache_enabled_for_members_only
+        event_manager_impl._enabled_for_event = mock.Mock(return_value=False)
+        gateway_guild = entity_factory.deserialize_gateway_guild.return_value
+        gateway_guild.members.return_value = {1: "member1", 2: "member2"}
+
+        mock_request_guild_members = mock.Mock()
+
+        with mock.patch.object(asyncio, "create_task") as create_task:
+            with mock.patch.object(event_manager, "_fixed_size_nonce", return_value="abc"):
+                with mock.patch.object(event_manager, "_request_guild_members", new=mock_request_guild_members):
+                    await event_manager_impl.on_guild_create(shard, {"id": 456, "large": False})
+
+        mock_request_guild_members.assert_not_called()
+        create_task.assert_not_called()
+
     @pytest.mark.asyncio()
     async def test_on_guild_create_when_members_declared_and_enabled_for_member_chunk_event(
         self, stateless_event_manager_impl, shard, event_factory, entity_factory
diff --git a/tests/hikari/impl/test_gateway_bot.py b/tests/hikari/impl/test_gateway_bot.py
index b854a3f7..6dd52a2c 100644
--- a/tests/hikari/impl/test_gateway_bot.py
+++ b/tests/hikari/impl/test_gateway_bot.py
@@ -55,20 +55,6 @@ def test_validate_activity_when_no_activity(activity):
     warn.assert_not_called()
 
 
-def test_validate_activity_when_type_is_custom():
-    activity = presences.Activity(name="test", type=presences.ActivityType.CUSTOM)
-
-    with mock.patch.object(warnings, "warn") as warn:
-        bot_impl._validate_activity(activity)
-
-    warn.assert_called_once_with(
-        "The CUSTOM activity type is not supported by bots at the time of writing, and may therefore not have "
-        "any effect if used.",
-        category=errors.HikariWarning,
-        stacklevel=3,
-    )
-
-
 def test_validate_activity_when_type_is_streaming_but_no_url():
     activity = presences.Activity(name="test", url=None, type=presences.ActivityType.STREAMING)
 
@@ -84,7 +70,7 @@ def test_validate_activity_when_type_is_streaming_but_no_url():
 
 
 def test_validate_activity_when_no_warning():
-    activity = presences.Activity(name="test", type=presences.ActivityType.PLAYING)
+    activity = presences.Activity(name="test", state="Hello!", type=presences.ActivityType.CUSTOM)
 
     with mock.patch.object(warnings, "warn") as warn:
         bot_impl._validate_activity(activity)
diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py
index 15132aa7..142f043f 100644
--- a/tests/hikari/impl/test_rest.py
+++ b/tests/hikari/impl/test_rest.py
@@ -1166,7 +1166,7 @@ def test__build_message_payload_with_undefined_args(self, rest_client):
             undefined.UNDEFINED, undefined.UNDEFINED, undefined.UNDEFINED, undefined.UNDEFINED
         )
 
-    @pytest.mark.parametrize("args", [("embeds", "components"), ("embed", "component")])
+    @pytest.mark.parametrize("args", [("embeds", "components", "attachments"), ("embed", "component", "attachment")])
     def test__build_message_payload_with_None_args(self, rest_client, args):
         kwargs = {}
         for arg in args:
@@ -1177,7 +1177,7 @@ def test__build_message_payload_with_None_args(self, rest_client, args):
         ) as generate_allowed_mentions:
             body, form = rest_client._build_message_payload(**kwargs)
 
-        assert body == {"embeds": [], "components": [], "allowed_mentions": {"allowed_mentions": 1}}
+        assert body == {"embeds": [], "components": [], "attachments": [], "allowed_mentions": {"allowed_mentions": 1}}
         assert form is None
 
         generate_allowed_mentions.assert_called_once_with(
diff --git a/tests/hikari/impl/test_shard.py b/tests/hikari/impl/test_shard.py
index 943376fd..5b10ab1b 100644
--- a/tests/hikari/impl/test_shard.py
+++ b/tests/hikari/impl/test_shard.py
@@ -57,9 +57,30 @@ def test__serialize_activity_when_activity_is_None():
 
 
 def test__serialize_activity_when_activity_is_not_None():
-    activity = mock.Mock(type="0", url="https://some.url")
-    activity.name = "some name"  # This has to be set separate because if not, its set as the mock's name
-    assert shard._serialize_activity(activity) == {"name": "some name", "type": 0, "url": "https://some.url"}
+    activity = presences.Activity(name="some name", type=0, state="blah", url="https://some.url")
+    assert shard._serialize_activity(activity) == {
+        "name": "some name",
+        "type": 0,
+        "state": "blah",
+        "url": "https://some.url",
+    }
+
+
+@pytest.mark.parametrize(
+    ("activity_name", "activity_state", "expected_name", "expected_state"),
+    [("Testing!", None, "Custom Status", "Testing!"), ("Blah name!", "Testing!", "Blah name!", "Testing!")],
+)
+def test__serialize_activity_custom_activity_syntactic_sugar(
+    activity_name, activity_state, expected_name, expected_state
+):
+    activity = presences.Activity(name=activity_name, state=activity_state, type=presences.ActivityType.CUSTOM)
+
+    assert shard._serialize_activity(activity) == {
+        "type": 4,
+        "name": expected_name,
+        "state": expected_state,
+        "url": None,
+    }
 
 
 def test__serialize_datetime_when_datetime_is_None():
@@ -598,7 +619,12 @@ def test__serialize_and_store_presence_payload_when_all_args_undefined(
         actual_result = client._serialize_and_store_presence_payload()
 
         if activity is not None:
-            expected_activity = {"name": activity.name, "type": activity.type, "url": activity.url}
+            expected_activity = {
+                "name": activity.name,
+                "state": activity.state,
+                "type": activity.type,
+                "url": activity.url,
+            }
         else:
             expected_activity = None