diff --git a/changelog/1047.bugfix.rst b/changelog/1047.bugfix.rst new file mode 100644 index 0000000000..e8a25b5829 --- /dev/null +++ b/changelog/1047.bugfix.rst @@ -0,0 +1 @@ +Fix permission resolution for :class:`Thread`\s to use :attr:`Permissions.send_messages_in_threads` instead of :attr:`Permissions.send_messages` for calculating implicit permissions. diff --git a/disnake/abc.py b/disnake/abc.py index 5288c6065a..c53915a8a9 100644 --- a/disnake/abc.py +++ b/disnake/abc.py @@ -633,6 +633,21 @@ def jump_url(self) -> str: """ return f"https://discord.com/channels/{self.guild.id}/{self.id}" + def _apply_implict_permissions(self, base: Permissions) -> None: + # if you can't send a message in a channel then you can't have certain + # permissions as well + if not base.send_messages: + base.send_tts_messages = False + base.send_voice_messages = False + base.mention_everyone = False + base.embed_links = False + base.attach_files = False + + # if you can't view a channel then you have no permissions there + if not base.view_channel: + denied = Permissions.all_channel() + base.value &= ~denied.value + def permissions_for( self, obj: Union[Member, Role], @@ -784,25 +799,11 @@ def permissions_for( base.handle_overwrite(allow=overwrite.allow, deny=overwrite.deny) break - # if you can't send a message in a channel then you can't have certain - # permissions as well - if not base.send_messages: - base.send_tts_messages = False - base.send_voice_messages = False - base.mention_everyone = False - base.embed_links = False - base.attach_files = False - - # if you can't view a channel then you have no permissions there - if not base.view_channel: - denied = Permissions.all_channel() - base.value &= ~denied.value - # if you have a timeout then you can't have any permissions # except read messages and read message history if not ignore_timeout and obj.current_timeout: - denied = Permissions(view_channel=True, read_message_history=True) - base.value &= denied.value + allowed = Permissions(view_channel=True, read_message_history=True) + base.value &= allowed.value return base diff --git a/disnake/channel.py b/disnake/channel.py index ea738f235c..9686e6cd71 100644 --- a/disnake/channel.py +++ b/disnake/channel.py @@ -263,6 +263,7 @@ def permissions_for( ignore_timeout: bool = MISSING, ) -> Permissions: base = super().permissions_for(obj, ignore_timeout=ignore_timeout) + self._apply_implict_permissions(base) # text channels do not have voice related permissions denied = Permissions.voice() @@ -1183,6 +1184,35 @@ def voice_states(self) -> Dict[int, VoiceState]: if value.channel and value.channel.id == self.id } + @utils.copy_doc(disnake.abc.GuildChannel.permissions_for) + def permissions_for( + self, + obj: Union[Member, Role], + /, + *, + ignore_timeout: bool = MISSING, + ) -> Permissions: + base = super().permissions_for(obj, ignore_timeout=ignore_timeout) + self._apply_implict_permissions(base) + + # voice channels cannot be edited by people who can't connect to them + # It also implicitly denies all other voice perms + if not base.connect: + denied = Permissions.voice() + # voice channels also deny all text related permissions + denied.value |= Permissions.text().value + # stage channels remove the stage permissions + denied.value |= Permissions.stage().value + + denied.update( + manage_channels=True, + manage_roles=True, + manage_events=True, + manage_webhooks=True, + ) + base.value &= ~denied.value + return base + class VoiceChannel(disnake.abc.Messageable, VocalGuildChannel): """Represents a Discord guild voice channel. @@ -1442,32 +1472,6 @@ def get_partial_message(self, message_id: int, /) -> PartialMessage: return PartialMessage(channel=self, id=message_id) - @utils.copy_doc(disnake.abc.GuildChannel.permissions_for) - def permissions_for( - self, - obj: Union[Member, Role], - /, - *, - ignore_timeout: bool = MISSING, - ) -> Permissions: - base = super().permissions_for(obj, ignore_timeout=ignore_timeout) - - # voice channels cannot be edited by people who can't connect to them - # It also implicitly denies all other voice perms - if not base.connect: - denied = Permissions.voice() - # voice channels also deny all text related permissions - denied.value |= Permissions.text().value - - denied.update( - manage_channels=True, - manage_roles=True, - manage_events=True, - manage_webhooks=True, - ) - base.value &= ~denied.value - return base - # if only these parameters are passed, `_move` is called and no channel will be returned @overload async def edit( @@ -2183,31 +2187,6 @@ def instance(self) -> Optional[StageInstance]: """ return utils.get(self.guild.stage_instances, channel_id=self.id) - @utils.copy_doc(disnake.abc.GuildChannel.permissions_for) - def permissions_for( - self, - obj: Union[Member, Role], - /, - *, - ignore_timeout: bool = MISSING, - ) -> Permissions: - base = super().permissions_for(obj, ignore_timeout=ignore_timeout) - - # voice channels cannot be edited by people who can't connect to them - # It also implicitly denies all other channel permissions. - if not base.connect: - denied = Permissions.voice() - denied.value |= Permissions.text().value - denied.value |= Permissions.stage().value - denied.update( - manage_channels=True, - manage_roles=True, - manage_events=True, - manage_webhooks=True, - ) - base.value &= ~denied.value - return base - async def create_instance( self, *, @@ -2787,6 +2766,19 @@ def type(self) -> Literal[ChannelType.category]: """ return ChannelType.category + @utils.copy_doc(disnake.abc.GuildChannel.permissions_for) + def permissions_for( + self, + obj: Union[Member, Role], + /, + *, + ignore_timeout: bool = MISSING, + ) -> Permissions: + base = super().permissions_for(obj, ignore_timeout=ignore_timeout) + self._apply_implict_permissions(base) + + return base + def is_nsfw(self) -> bool: """Whether the category is marked as NSFW. @@ -3329,6 +3321,7 @@ def permissions_for( ignore_timeout: bool = MISSING, ) -> Permissions: base = super().permissions_for(obj, ignore_timeout=ignore_timeout) + self._apply_implict_permissions(base) # forum channels do not have voice related permissions denied = Permissions.voice() diff --git a/disnake/threads.py b/disnake/threads.py index 56822dbdfb..2126e85605 100644 --- a/disnake/threads.py +++ b/disnake/threads.py @@ -6,7 +6,7 @@ import time from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Literal, Optional, Sequence, Union -from .abc import Messageable +from .abc import GuildChannel, Messageable from .enums import ChannelType, ThreadArchiveDuration, try_enum, try_enum_to_int from .errors import ClientException from .flags import ChannelFlags @@ -423,9 +423,15 @@ def permissions_for( or :class:`~disnake.Role`. Since threads do not have their own permissions, they inherit them - from the parent channel. This is a convenience method for - calling :meth:`~disnake.TextChannel.permissions_for` on the - parent channel. + from the parent channel. + However, the permission context is different compared to a normal channel, + so this method has different behavior than calling the parent's + :attr:`GuildChannel.permissions_for <.abc.GuildChannel.permissions_for>` + method directly. + + .. versionchanged:: 2.9 + Properly takes :attr:`Permissions.send_messages_in_threads` + into consideration. Parameters ---------- @@ -460,7 +466,24 @@ def permissions_for( parent = self.parent if parent is None: raise ClientException("Parent channel not found") - return parent.permissions_for(obj, ignore_timeout=ignore_timeout) + # n.b. GuildChannel is used here so implicit overrides are not applied based on send_messages + base = GuildChannel.permissions_for(parent, obj, ignore_timeout=ignore_timeout) + + # if you can't send a message in a channel then you can't have certain + # permissions as well + if not base.send_messages_in_threads: + base.send_tts_messages = False + base.send_voice_messages = False + base.mention_everyone = False + base.embed_links = False + base.attach_files = False + + # if you can't view a channel then you have no permissions there + if not base.view_channel: + denied = Permissions.all_channel() + base.value &= ~denied.value + + return base async def delete_messages(self, messages: Iterable[Snowflake]) -> None: """|coro|