Skip to content

Commit c3fdf88

Browse files
committed
feat: Added support to edit guild features
1 parent adff41d commit c3fdf88

File tree

8 files changed

+193
-19
lines changed

8 files changed

+193
-19
lines changed

discord/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from .activity import *
3232
from .channel import *
3333
from .components import *
34-
from .guild import Guild
34+
from .guild import *
3535
from .flags import *
3636
from .member import Member, VoiceState
3737
from .message import *

discord/automod.py

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,31 +70,66 @@ class AutoModAction:
7070
"""
7171
Represents an action which will execute whenever a rule is triggered.
7272
73-
Parameters
73+
Attributes
7474
-----------
7575
type: :class:`AutoModActionType`
7676
The type of action
77-
channel_id: Optional[:class:`int`]
78-
The channel to which user content should be logged.
77+
channel_id: :class:`int`
78+
The channel to which target user content should be logged.
7979
8080
.. note::
81-
This field is only required :attr:`~AutoModAction.type` is :attr:~`AutoModActionType.send_alert_message`
82-
83-
timeout_duration: Optional[Union[:class:`int`, :class:`datetime.timedelta`]]
84-
Duration in seconds (:class:`int`) or a timerange (:class:`~datetime.timedelta`) for wich the user should be timeouted.
81+
This field is only present if :attr:`~AutoModAction.type` is :attr:`~AutoModActionType.send_alert_message`
82+
83+
timeout_duration: Union[:class:`int`, :class:`~datetime.timedelta`]
84+
Duration in seconds (:class:`int`) or a timerange (:class:`~datetime.timedelta`) for wich the target user should be timeouted.
8585
8686
**The maximum value is** ``2419200`` **seconds (4 weeks)**
8787
8888
.. note::
89-
This field is only required if :attr:`type` is :attr:`AutoModActionType.timeout_user`
89+
This field is only present if :attr:`~AutoModAction.type` is :attr:`~AutoModActionType.timeout_user`
9090
9191
custom_message: Optional[:class:`str`]
92-
Additional explanation that will be shown to members whenever their message is blocked. **Max 150 characters**
92+
Additional explanation that will be shown to target users whenever their message is blocked. **Max 150 characters**
9393
9494
.. note::
95-
This field is only allowed if :attr:`type` is :attr:`AutoModActionType.block_message`
95+
This field might only be present if :attr:`~AutoModAction.type` is :attr:`~AutoModActionType.block_message`
9696
"""
9797
def __init__(self, type: AutoModActionType, **metadata):
98+
"""
99+
100+
Parameters
101+
-----------
102+
type: :class:`AutoModActionType`
103+
The type of action
104+
105+
channel_id: :class:`int`
106+
The channel to which target user content should be logged.
107+
108+
.. note::
109+
This field is only required if :attr:`~AutoModAction.type` is :attr:`~AutoModActionType.send_alert_message`
110+
111+
timeout_duration: Union[:class:`int`, :class:`datetime.timedelta`]
112+
Duration in seconds (:class:`int`) or a timerange (:class:`~datetime.timedelta`) for wich the user should be timeouted.
113+
114+
**The maximum value is** ``2419200`` **seconds (4 weeks)**
115+
116+
.. note::
117+
This field is only required if :attr:`~AutoModAction.type` is :attr:`~AutoModActionType.timeout_user`
118+
119+
custom_message: Optional[:class:`str`]
120+
Additional explanation that will be shown to target users whenever their message is blocked. **Max 150 characters**
121+
122+
.. note::
123+
This field is only allowed if :attr:`~AutoModAction.type` is :attr:`~AutoModActionType.block_message`
124+
125+
Raises
126+
-------
127+
TypeError
128+
If the type is :attr:`~AutoModActionType.send_alert_message` and no ``channel_id`` is provided,
129+
or if the type is :attr:`~AutoModActionType.timeout_user` and no ``timeout_duration`` is provided.
130+
ValueError
131+
If the ``custom_message`` is longer than 150 characters, or if the ``timeout_duration`` is longer than 4 weeks.
132+
"""
98133
self.type: AutoModActionType = try_enum(AutoModActionType, type)
99134
self.metadata = metadata # maybe we need this later... idk
100135

discord/guild.py

Lines changed: 142 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@
3636
Dict,
3737
Any,
3838
Awaitable,
39+
Iterable,
40+
Iterator,
3941
NamedTuple,
4042
TYPE_CHECKING
4143
)
4244
from typing_extensions import Literal
4345

44-
from discord.utils import _bytes_to_base64_data
45-
4646
if TYPE_CHECKING:
4747
from os import PathLike
4848
from .state import ConnectionState
@@ -93,6 +93,13 @@
9393
from .automod import AutoModRule, AutoModTriggerMetadata, AutoModAction
9494
from .application_commands import SlashCommand, MessageCommand, UserCommand, Localizations
9595

96+
MISSING = utils.MISSING
97+
98+
__all__ = (
99+
'GuildFeatures',
100+
'Guild',
101+
)
102+
96103

97104
class _GuildLimit(NamedTuple):
98105
emoji: int
@@ -101,9 +108,6 @@ class _GuildLimit(NamedTuple):
101108
filesize: int
102109

103110

104-
MISSING = utils.MISSING
105-
106-
107111
async def default_callback(interaction, *args, **kwargs):
108112
await interaction.respond(
109113
'This command has no callback set.'
@@ -112,6 +116,129 @@ async def default_callback(interaction, *args, **kwargs):
112116
)
113117

114118

119+
class GuildFeatures(Iterable[str], dict):
120+
"""
121+
Represents a guild's features.
122+
123+
This class mainly exists to make it easier to edit a guild's features.
124+
125+
.. versionadded:: 2.0
126+
127+
.. container:: operations
128+
129+
.. describe:: 'FEATURE_NAME' in features
130+
131+
Checks if the guild has the feature.
132+
133+
.. describe:: features.FEATURE_NAME
134+
135+
Checks if the guild has the feature. Returns ``False`` if it doesn't.
136+
137+
.. describe:: features.FEATURE_NAME = True
138+
139+
Enables the feature in the features object, but does not enable it in the guild itself except if you pass it to :meth:`Guild.edit`.
140+
141+
.. describe:: features.FEATURE_NAME = False
142+
143+
Disables the feature in the features object, but does not disable it in the guild itself except if you pass it to :meth:`Guild.edit`.
144+
145+
.. describe:: del features.FEATURE_NAME
146+
147+
The same as ``features.FEATURE_NAME = False``
148+
149+
.. describe:: features.parsed()
150+
151+
Returns a list of all features that are/should be enabled.
152+
153+
.. describe:: features.merge(other)
154+
155+
Returns a new object with the features of both objects merged.
156+
If a feature is missing in the other object, it will be ignored.
157+
158+
.. describe:: features == other
159+
160+
Checks if two feature objects are equal.
161+
162+
.. describe:: features != other
163+
164+
Checks if two feature objects are not equal.
165+
166+
.. describe:: iter(features)
167+
168+
Returns an iterator over the enabled features.
169+
"""
170+
def __init__(self, /, initial: List[str], **features: bool):
171+
"""
172+
Parameters
173+
-----------
174+
initial: :class:`list`
175+
The initial features to set.
176+
**features: :class:`bool`
177+
The features to set. If the value is ``True`` then the feature is/will be enabled.
178+
If the value is ``False`` then the feature will be disabled.
179+
"""
180+
for feature in initial:
181+
features[feature] = True
182+
self.__dict__.update(features)
183+
184+
def __iter__(self) -> Iterator[str]:
185+
return [feature for feature, value in self.__dict__.items() if value is True].__iter__()
186+
187+
def __contains__(self, item: str) -> bool:
188+
return item in self.__dict__ and self.__dict__[item] is True
189+
190+
def __getattr__(self, item: str) -> bool:
191+
return self.__dict__.get(item, False)
192+
193+
def __setattr__(self, key: str, value: bool) -> None:
194+
self.__dict__[key] = value
195+
196+
def __delattr__(self, item: str) -> None:
197+
self.__dict__[item] = False
198+
199+
def __repr__(self) -> str:
200+
return f'<GuildFeatures {self.__dict__!r}>'
201+
202+
def __str__(self) -> str:
203+
return str(self.__dict__)
204+
205+
def merge(self, other: GuildFeatures) -> GuildFeatures:
206+
base = copy.copy(self.__dict__)
207+
for key, value in other.items():
208+
base[key] = value
209+
210+
return GuildFeatures(**base)
211+
212+
def parsed(self) -> List[str]:
213+
return [name for name, value in self.__dict__.items() if value is True]
214+
215+
def __eq__(self, other: GuildFeatures) -> bool:
216+
current = self.__dict__
217+
other = other.__dict__
218+
219+
all_keys = set(current.keys()) | set(other.keys())
220+
221+
for key in all_keys:
222+
try:
223+
current_value = current[key]
224+
except KeyError:
225+
if other[key] is True:
226+
return False
227+
else:
228+
try:
229+
other_value = other[key]
230+
except KeyError:
231+
pass
232+
else:
233+
if current_value != other_value:
234+
return False
235+
236+
return True
237+
238+
def __ne__(self, other: GuildFeatures) -> bool:
239+
return not self.__eq__(other)
240+
241+
115242
class Guild(Hashable):
116243
"""Represents a Discord guild.
117244
@@ -1486,6 +1613,7 @@ async def edit(
14861613
self,
14871614
name: str = MISSING,
14881615
description: str = MISSING,
1616+
features: GuildFeatures = MISSING,
14891617
icon: Optional[bytes] = MISSING,
14901618
banner: Optional[bytes] = MISSING,
14911619
splash: Optional[bytes] = MISSING,
@@ -1523,6 +1651,10 @@ async def edit(
15231651
description: :class:`str`
15241652
The new description of the guild. This is only available to guilds that
15251653
contain ``PUBLIC`` in :attr:`Guild.features`.
1654+
features: :class:`GuildFeatures`
1655+
Features to enable/disable will be merged in to the current features.
1656+
See the `discord api documentation <https://discord.com/developers/docs/resources/guild#guild-object-mutable-guild-features>`_
1657+
for a list of currently mutable features and the required permissions.
15261658
icon: :class:`bytes`
15271659
A :term:`py:bytes-like object` representing the icon. Only PNG/JPEG is supported.
15281660
GIF is only available to guilds that contain ``ANIMATED_ICON`` in :attr:`Guild.features`.
@@ -1605,6 +1737,10 @@ async def edit(
16051737
if splash is not MISSING:
16061738
fields['splash'] = utils._bytes_to_base64_data(splash)
16071739

1740+
if features is not MISSING:
1741+
current_features = GuildFeatures(self.features)
1742+
fields['features'] = current_features.merge(features)
1743+
16081744
if discovery_splash is not MISSING:
16091745
fields['discovery_splash'] = utils._bytes_to_base64_data(discovery_splash)
16101746

@@ -3154,7 +3290,7 @@ async def create_scheduled_event(
31543290
if cover_image:
31553291
if not isinstance(cover_image, bytes):
31563292
raise ValueError(f'cover_image must be of type bytes, not {cover_image.__class__.__name__}')
3157-
as_base64 = _bytes_to_base64_data(cover_image)
3293+
as_base64 = utils._bytes_to_base64_data(cover_image)
31583294
fields['image'] = as_base64
31593295

31603296
data = await self._state.http.create_guild_event(guild_id=self.id, fields=fields, reason=reason)

discord/http.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -990,7 +990,7 @@ def edit_voice_state(self, guild_id, user_id, payload):
990990
r = Route('PATCH', '/guilds/{guild_id}/voice-states/{user_id}', guild_id=guild_id, user_id=user_id)
991991
return self.request(r, json=payload)
992992

993-
def edit_member(self, guild_id, user_id, *, reason=None, fields):
993+
def edit_member(self, guild_id, user_id, *, reason=None, **fields):
994994
r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id)
995995
return self.request(r, json=fields, reason=reason)
996996

docs/api.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3295,6 +3295,9 @@ Guild
32953295
.. automethod:: audit_logs
32963296
:async-for:
32973297

3298+
.. autoclass:: GuildFeatures()
3299+
:members:
3300+
32983301
.. class:: BanEntry
32993302

33003303
A namedtuple which represents a ban returned from :meth:`~Guild.bans`.

docs/discord.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Creating a Bot account is a pretty straightforward process.
99

1010
1. Make sure you're logged on to the `Discord website <https://discord.com>`_.
1111
2. Navigate to the `application page <https://discord.com/developers/applications>`_
12-
3. Click on the "New Application" button.
12+
3. Click on the `New Application <https://discord.com/developers/applications?new_application=true>`_ button.
1313

1414
.. image:: /images/discord_create_app_button.png
1515
:alt: The new application button.
7.27 KB
Loading
14.8 KB
Loading

0 commit comments

Comments
 (0)