diff --git a/discord/channel.py b/discord/channel.py index 89a119ede..1d06db971 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -2498,12 +2498,7 @@ class DMChannel(discord.abc.Messageable, discord.abc.PrivateChannel, Hashable): :class:`Permissions` The resolved permissions. """ - - base = Permissions.text() - base.read_messages = True - base.send_tts_messages = False - base.manage_messages = False - return base + return Permissions._dm_permissions() def get_partial_message(self, message_id: int, /) -> PartialMessage: """Creates a :class:`PartialMessage` from the message ID. @@ -2671,10 +2666,7 @@ class GroupChannel(discord.abc.Messageable, discord.abc.PrivateChannel, Hashable The resolved permissions for the user. """ - base = Permissions.text() - base.read_messages = True - base.send_tts_messages = False - base.manage_messages = False + base = Permissions._dm_permissions() base.mention_everyone = True if obj.id == self.owner_id: diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index f910dc2b3..c70ed31cf 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -28,7 +28,7 @@ from typing import TYPE_CHECKING, Any, Dict, Generator, Generic, List, Optional, import discord.abc import discord.utils -from discord import Interaction, Message, Attachment, MessageType, User, PartialMessageable +from discord import Interaction, Message, Attachment, MessageType, User, PartialMessageable, Permissions, ChannelType, Thread from discord.context_managers import Typing from .view import StringView @@ -456,6 +456,76 @@ class Context(discord.abc.Messageable, Generic[BotT]): # bot.user will never be None at this point. return self.guild.me if self.guild is not None else self.bot.user # type: ignore + @discord.utils.cached_property + def permissions(self) -> Permissions: + """:class:`.Permissions`: Returns the resolved permissions for the invoking user in this channel. + Shorthand for :meth:`.abc.GuildChannel.permissions_for` or :attr:`.Interaction.permissions`. + + .. versionadded:: 2.0 + """ + if self.channel.type is ChannelType.private: + return Permissions._dm_permissions() + if not self.interaction: + # channel and author will always match relevant types here + return self.channel.permissions_for(self.author) # type: ignore + base = self.interaction.permissions + if self.channel.type in (ChannelType.voice, ChannelType.stage_voice): + if not base.connect: + # voice channels cannot be edited by people who can't connect to them + # It also implicitly denies all other voice perms + denied = Permissions.voice() + denied.update(manage_channels=True, manage_roles=True) + base.value &= ~denied.value + else: + # text channels do not have voice related permissions + denied = Permissions.voice() + base.value &= ~denied.value + return base + + @discord.utils.cached_property + def bot_permissions(self) -> Permissions: + """:class:`.Permissions`: Returns the resolved permissions for the bot in this channel. + Shorthand for :meth:`.abc.GuildChannel.permissions_for` or :attr:`.Interaction.app_permissions`. + + For interaction-based commands, this will reflect the effective permissions + for :class:`Context` calls, which may differ from calls through + other :class:`.abc.Messageable` endpoints, like :attr:`channel`. + + Notably, sending messages, embedding links, and attaching files are always + permitted, while reading messages might not be. + + .. versionadded:: 2.0 + """ + channel = self.channel + if channel.type == ChannelType.private: + return Permissions._dm_permissions() + if not self.interaction: + # channel and me will always match relevant types here + return channel.permissions_for(self.me) # type: ignore + guild = channel.guild + base = self.interaction.app_permissions + if self.channel.type in (ChannelType.voice, ChannelType.stage_voice): + if not base.connect: + # voice channels cannot be edited by people who can't connect to them + # It also implicitly denies all other voice perms + denied = Permissions.voice() + denied.update(manage_channels=True, manage_roles=True) + base.value &= ~denied.value + else: + # text channels do not have voice related permissions + denied = Permissions.voice() + base.value &= ~denied.value + base.update( + embed_links=True, + attach_files=True, + send_tts_messages=False, + ) + if isinstance(channel, Thread): + base.send_messages_in_threads = True + else: + base.send_messages = True + return base + @property def voice_client(self) -> Optional[VoiceProtocol]: r"""Optional[:class:`.VoiceProtocol`]: A shortcut to :attr:`.Guild.voice_client`\, if applicable.""" diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index bfd140437..e76edf39f 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -2159,8 +2159,7 @@ def has_permissions(**perms: bool) -> Check[Any]: raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") def predicate(ctx: Context[BotT]) -> bool: - ch = ctx.channel - permissions = ch.permissions_for(ctx.author) # type: ignore + permissions = ctx.permissions missing = [perm for perm, value in perms.items() if getattr(permissions, perm) != value] @@ -2185,9 +2184,7 @@ def bot_has_permissions(**perms: bool) -> Check[Any]: raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") def predicate(ctx: Context[BotT]) -> bool: - guild = ctx.guild - me = guild.me if guild is not None else ctx.bot.user - permissions = ctx.channel.permissions_for(me) # type: ignore + permissions = ctx.bot_permissions missing = [perm for perm, value in perms.items() if getattr(permissions, perm) != value] diff --git a/discord/permissions.py b/discord/permissions.py index 8b8c93fd5..c20cd854d 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -186,6 +186,14 @@ class Permissions(BaseFlags): p.read_message_history = False return ~p.value + @classmethod + def _dm_permissions(cls) -> Self: + base = cls.text() + base.read_messages = True + base.send_tts_messages = False + base.manage_messages = False + return base + @classmethod def all_channel(cls) -> Self: """A :class:`Permissions` with all channel-specific permissions set to