Repository: hikari-py/hikari (python)

Issue:
Support editing community features in `edit_guild` endpoint
### Summary

A guild's Community mode can be changed by passing `community: bool` arg to `edit_guild()`, similar to that of [discord.py](https://discordpy.readthedocs.io/en/stable/api.html#discord.Guild.edit)

### Why is this needed?

The feature is documented in the [official Discord API](https://discord.com/developers/docs/resources/guild#modify-guild) and is currently missing in `hikari`. Implementation would allow for a more automated setting up considering various Community-only features in hikari such as `system_channel`, `rule_channel`, `public_updates_channel`, `preferred_locale`

### Ideal implementation

A naive bandaging implementation would be
```py
...
    async def edit_guild(
        ...
        community: undefined.UndefinedNoneOr[bool] = undefined.UNDEFINED,
        ...
    ) -> guilds.RESTGuild:
        # Naive implementation
        ...
        if not isinstance(community, undefined.UNDEFINED):
            features = set(guild.features)
            if community
                features.add(guilds.GuildFeature.COMMUNITY)
            else:
                features.discard(guilds.GuildFeature.COMMUNITY)

            body.put("features", features, conversion=list)  # Convert just in case
        ...
...
```
I opted for a `set()` intermediate here because:
- `set()` safeguards against duplicate
- `set.discard()` doesn't throw an error if the value we're discarding doesn't exist.

I also do not consider my naive implementation to be "elegant".

### Checklist

- [X] I have searched the issue tracker and have made sure it's not a duplicate. If it is a follow up of another issue, I have specified it.

Pull request:
Allow changing guild features (community, etc.)
### Summary
This PR adds two missing mutable guild features to `GuildFeature` enums, introduces `MutableGuildFeature` for ease of use, and a `features` argument to `edit_guild()` to address the lack of support for editing guild features. Relevant docstrings and tests had been updated accordingly.

### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [x] I have run `nox` and all the pipelines have passed.
- [x] I have made unittests according to the code I have added/modified/deleted.

### Related issues
<!--
To mention an issue use `#issue-id` and to mention a merge request use `!merge-request-id`
To close/fix an issue use `Close #issue-id` or `Fix #issue-id` (depending on the merge request)
-->
Resolves #1768.
Partially addresses #1827.

Patch:
diff --git a/changes/1828.feature.md b/changes/1828.feature.md
new file mode 100644
index 00000000..b6eb11a0
--- /dev/null
+++ b/changes/1828.feature.md
@@ -0,0 +1,1 @@
+Allow changing guild features (community, etc.)
diff --git a/dev-requirements/formatting.txt b/dev-requirements/formatting.txt
index b06924c7..ba49b8f7 100644
--- a/dev-requirements/formatting.txt
+++ b/dev-requirements/formatting.txt
@@ -1,2 +1,2 @@
-black==24.1.1
+black==24.2.0
 isort==5.13.2
diff --git a/dev-requirements/pyright.txt b/dev-requirements/pyright.txt
index 94a71c3d..fbbea93f 100644
--- a/dev-requirements/pyright.txt
+++ b/dev-requirements/pyright.txt
@@ -1,1 +1,1 @@
-pyright==1.1.349
+pyright==1.1.351
diff --git a/dev-requirements/release.txt b/dev-requirements/release.txt
index d0c68f8e..32d15c7b 100644
--- a/dev-requirements/release.txt
+++ b/dev-requirements/release.txt
@@ -1,3 +1,3 @@
-twine==4.0.2
+twine==5.0.0
 
 -r build.txt
diff --git a/dev-requirements/slotscheck.txt b/dev-requirements/slotscheck.txt
index 084deacb..75723110 100644
--- a/dev-requirements/slotscheck.txt
+++ b/dev-requirements/slotscheck.txt
@@ -1,1 +1,1 @@
-slotscheck==0.17.1
+slotscheck==0.17.3
diff --git a/hikari/api/rest.py b/hikari/api/rest.py
index 26752978..4fb138f0 100644
--- a/hikari/api/rest.py
+++ b/hikari/api/rest.py
@@ -3889,6 +3889,7 @@ async def edit_guild(
             snowflakes.SnowflakeishOr[channels_.GuildTextChannel]
         ] = undefined.UNDEFINED,
         preferred_locale: undefined.UndefinedOr[typing.Union[str, locales.Locale]] = undefined.UNDEFINED,
+        features: undefined.UndefinedOr[typing.Sequence[guilds.GuildFeature]] = undefined.UNDEFINED,
         reason: undefined.UndefinedOr[str] = undefined.UNDEFINED,
     ) -> guilds.RESTGuild:
         """Edit a guild.
@@ -3936,6 +3937,15 @@ async def edit_guild(
             If provided, the new public updates channel.
         preferred_locale : hikari.undefined.UndefinedNoneOr[str]
             If provided, the new preferred locale.
+        features : hikari.undefined.UndefinedOr[typing.Sequence[hikari.guilds.GuildFeature]]
+            If provided, the guild features to be enabled. Features not provided will be disabled.
+
+            .. warning::
+                At the time of writing, Discord ignores non-`mutable features
+                <https://discord.com/developers/docs/resources/guild#guild-object-mutable-guild-features>`_.
+                This behaviour can change in the future. You should refer to the
+                aforementioned link for the most up-to-date information, and
+                only supply mutable features.
         reason : hikari.undefined.UndefinedOr[str]
             If provided, the reason that will be recorded in the audit logs.
             Maximum of 512 characters.
diff --git a/hikari/guilds.py b/hikari/guilds.py
index c570efe0..c76ca24a 100644
--- a/hikari/guilds.py
+++ b/hikari/guilds.py
@@ -183,6 +183,12 @@ class GuildFeature(str, enums.Enum):
     ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE = "ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE"
     """Guild has role subscriptions available for purchase."""
 
+    INVITES_DISABLED = "INVITES_DISABLED"
+    """Guild has paused invites, preventing new users from joining."""
+
+    RAID_ALERTS_DISABLED = "RAID_ALERTS_DISABLED"
+    """Guild has disabled alerts for join raids in the configured safety alerts channel."""
+
 
 @typing.final
 class GuildMessageNotificationsLevel(int, enums.Enum):
@@ -1548,6 +1554,7 @@ async def edit(
             snowflakes.SnowflakeishOr[channels_.GuildTextChannel]
         ] = undefined.UNDEFINED,
         preferred_locale: undefined.UndefinedOr[typing.Union[str, locales.Locale]] = undefined.UNDEFINED,
+        features: undefined.UndefinedOr[typing.Sequence[GuildFeature]] = undefined.UNDEFINED,
         reason: undefined.UndefinedOr[str] = undefined.UNDEFINED,
     ) -> RESTGuild:
         """Edit the guild.
@@ -1589,6 +1596,8 @@ async def edit(
             If provided, the new public updates channel.
         preferred_locale : hikari.undefined.UndefinedNoneOr[str]
             If provided, the new preferred locale.
+        features : hikari.undefined.UndefinedOr[typing.Sequence[hikari.guilds.GuildFeatures]]
+            If provided, the guild features to be enabled. Features not provided will be disabled.
         reason : hikari.undefined.UndefinedOr[str]
             If provided, the reason that will be recorded in the audit logs.
             Maximum of 512 characters.
@@ -1632,6 +1641,7 @@ async def edit(
             rules_channel=rules_channel,
             public_updates_channel=public_updates_channel,
             preferred_locale=preferred_locale,
+            features=features,
             reason=reason,
         )
 
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index 89b1905a..f2957c63 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -2544,6 +2544,7 @@ async def edit_guild(
             snowflakes.SnowflakeishOr[channels_.GuildTextChannel]
         ] = undefined.UNDEFINED,
         preferred_locale: undefined.UndefinedOr[typing.Union[str, locales.Locale]] = undefined.UNDEFINED,
+        features: undefined.UndefinedOr[typing.Sequence[typing.Union[str, guilds.GuildFeature]]] = undefined.UNDEFINED,
         reason: undefined.UndefinedOr[str] = undefined.UNDEFINED,
     ) -> guilds.RESTGuild:
         route = routes.PATCH_GUILD.compile(guild=guild)
@@ -2554,6 +2555,7 @@ async def edit_guild(
         body.put("explicit_content_filter", explicit_content_filter_level)
         body.put("afk_timeout", afk_timeout, conversion=time.timespan_to_int)
         body.put("preferred_locale", preferred_locale, conversion=str)
+        body.put_array("features", features, conversion=str)
         body.put_snowflake("afk_channel_id", afk_channel)
         body.put_snowflake("owner_id", owner)
         body.put_snowflake("system_channel_id", system_channel)


Test patch:
diff --git a/dev-requirements/pytest.txt b/dev-requirements/pytest.txt
index 5a2cafad..e2baf102 100644
--- a/dev-requirements/pytest.txt
+++ b/dev-requirements/pytest.txt
@@ -1,8 +1,8 @@
 # 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.4
-pytest-asyncio==0.23.4
+pytest==8.0.1
+pytest-asyncio==0.23.5
 pytest-cov==4.1.0
 pytest-randomly==3.15.0
 
diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py
index 6fa01dd7..88f7e758 100644
--- a/tests/hikari/impl/test_rest.py
+++ b/tests/hikari/impl/test_rest.py
@@ -4008,6 +4008,7 @@ async def test_edit_guild(self, rest_client, file_resource):
             "icon": "icon data",
             "splash": "splash data",
             "banner": "banner data",
+            "features": ["COMMUNITY", "RAID_ALERTS_DISABLED"],
         }
         rest_client._request = mock.AsyncMock(return_value={"id": "123"})
 
@@ -4028,6 +4029,7 @@ async def test_edit_guild(self, rest_client, file_resource):
                 rules_channel=StubModel(987),
                 public_updates_channel=(654),
                 preferred_locale="en-UK",
+                features=[guilds.GuildFeature.COMMUNITY, guilds.GuildFeature.RAID_ALERTS_DISABLED],
                 reason="hikari best",
             )
             assert result is rest_client._entity_factory.deserialize_rest_guild.return_value
@@ -4052,6 +4054,7 @@ async def test_edit_guild_when_images_are_None(self, rest_client):
             "icon": None,
             "splash": None,
             "banner": None,
+            "features": ["COMMUNITY", "RAID_ALERTS_DISABLED"],
         }
         rest_client._request = mock.AsyncMock(return_value={"ok": "NO"})
 
@@ -4071,6 +4074,7 @@ async def test_edit_guild_when_images_are_None(self, rest_client):
             rules_channel=StubModel(987),
             public_updates_channel=(654),
             preferred_locale="en-UK",
+            features=[guilds.GuildFeature.COMMUNITY, guilds.GuildFeature.RAID_ALERTS_DISABLED],
             reason="hikari best",
         )
         assert result is rest_client._entity_factory.deserialize_rest_guild.return_value
diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py
index 1c07054b..92091d26 100644
--- a/tests/hikari/test_guilds.py
+++ b/tests/hikari/test_guilds.py
@@ -629,6 +629,7 @@ async def test_edit(self, model):
             owner=6996,
             afk_timeout=400,
             preferred_locale="us-en",
+            features=[guilds.GuildFeature.COMMUNITY, guilds.GuildFeature.RAID_ALERTS_DISABLED],
             reason="beep boop",
         )
 
@@ -648,6 +649,7 @@ async def test_edit(self, model):
             rules_channel=undefined.UNDEFINED,
             public_updates_channel=undefined.UNDEFINED,
             preferred_locale="us-en",
+            features=[guilds.GuildFeature.COMMUNITY, guilds.GuildFeature.RAID_ALERTS_DISABLED],
             reason="beep boop",
         )