Ruby-Cogs/viewpermissions/viewpermissions.py
2025-04-02 22:57:51 -04:00

362 lines
16 KiB
Python

from AAA3A_utils import Cog # isort:skip
from redbot.core import commands # isort:skip
from redbot.core.bot import Red # isort:skip
from redbot.core.i18n import Translator, cog_i18n # isort:skip
import discord # isort:skip
import typing # isort:skip
import datetime
from redbot.core.utils.chat_formatting import humanize_list, pagify
from .view import PermissionsView
# Credits:
# General repo credits.
_: Translator = Translator("ViewPermissions", __file__)
class PermissionConverter(commands.Converter):
async def convert(self, ctx: commands.Context, argument: str) -> str:
argument = argument.strip().lower()
if argument not in discord.Permissions.VALID_FLAGS:
raise commands.BadArgument(
_("`{argument}` isn't a valid permission name").format(argument=argument)
)
return argument
@cog_i18n(_)
class ViewPermissions(Cog):
"""A cog to display permissions for roles and members, at guild level or in a specified channel!"""
async def get_permissions(
self,
guild: discord.Guild,
roles: typing.List[discord.Role] = None,
members: typing.List[discord.Member] = None,
channel: typing.Optional[discord.abc.GuildChannel] = None,
permissions: typing.List[str] = None,
) -> typing.Dict[
str,
typing.Dict[typing.Literal["qualified_name", "value", "source"], typing.Union[str, bool]],
]:
roles = [] if roles is None else roles.copy()
if members is None:
members = []
for member in members:
roles.extend(member.roles)
roles = sorted(set(roles))
if permissions is None:
permissions = []
sources = {}
# Thanks dpy for that (https://github.com/Rapptz/discord.py/blob/master/discord/abc.py#L666-L798)!
if any(
member == guild.owner for member in members
): # Guild owner get all permissions -- no questions asked. Otherwise...:
base = discord.Permissions.all()
for permission_name in dict(discord.Permissions.all()):
sources[permission_name] = "Guild owner."
else:
base = discord.Permissions(
guild.default_role.permissions.value
) # The @everyone role gets the first application.
# Apply guild roles that the member has.
for role in roles:
base.value |= role._permissions
for permission_name, value in dict(discord.Permissions(role._permissions)).items():
if value:
sources[permission_name] = f"{role.mention} ({role.id})"
# Guild-wide Administrator -> True for everything.
# Bypass all channel-specific overrides.
if base.administrator:
base = discord.Permissions.all()
for permission_name in dict(discord.Permissions.all()):
sources[permission_name] = "Guild administrator."
elif channel is not None:
# Apply @everyone allow/deny first since it's special.
try:
maybe_everyone = channel._overwrites[0]
if maybe_everyone.id == guild.id:
base.handle_overwrite(allow=maybe_everyone.allow, deny=maybe_everyone.deny)
for permission_name, value in dict(
discord.Permissions(maybe_everyone.allow)
).items():
if value:
sources[permission_name] = "Everyone channel overwrite."
remaining_overwrites = channel._overwrites[1:]
else:
remaining_overwrites = channel._overwrites
except IndexError:
remaining_overwrites = channel._overwrites
denies = 0
allows = 0
# Apply channel specific role permission overwrites.
roles_ids = [role.id for role in roles]
for overwrite in remaining_overwrites:
if overwrite.is_role() and overwrite.id in roles_ids:
denies |= overwrite.deny
allows |= overwrite.allow
for permission_name, value in dict(
discord.Permissions(overwrite.allow)
).items():
if value:
role = discord.utils.get(roles, id=overwrite.id)
sources[
permission_name
] = f"Role {role.mention} channel overwrite."
base.handle_overwrite(allow=allows, deny=denies)
# Apply member specific permission overwrites.
members_ids = [member.id for member in members]
for overwrite in remaining_overwrites:
if overwrite.is_member() and overwrite.id in members_ids:
base.handle_overwrite(allow=overwrite.allow, deny=overwrite.deny)
for permission_name, value in dict(
discord.Permissions(overwrite.allow)
).items():
if value:
sources[permission_name] = "Member channel overwrite."
break
if any(member.is_timed_out() for member in members):
# Timeout leads to every permission except VIEW_CHANNEL and READ_MESSAGE_HISTORY being explicitly denied.
# N.B.: This *must* come last, because it's a conclusive mask.
base.value &= discord.Permissions._timeout_mask()
# Apply implicit channel permissions.
channel._apply_implicit_permissions(base)
if isinstance(channel, (discord.TextChannel, discord.ForumChannel)):
base.value &= (
~discord.Permissions.voice().value
) # Text channels do not have voice related permissions.
elif isinstance(channel, discord.VoiceChannel):
# 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 = discord.Permissions.voice()
denied.update(manage_channels=True, manage_roles=True)
base.value &= ~denied.value
permissions_values = [
discord.Permissions.VALID_FLAGS[permission_name] for permission_name in permissions
]
permissions_dict = {
permission_name: {
"qualified_name": (
(
[
p
for p in discord.Permissions.VALID_FLAGS
if discord.Permissions.VALID_FLAGS[p]
== discord.Permissions.VALID_FLAGS[permission_name]
][-1]
.replace("_", " ")
.title()
)
if permission_name != "manage_roles"
else permission_name.replace("_", " ").title()
),
"value": value,
"source": sources.get(permission_name) if value else None,
}
for permission_name, value in dict(base).items()
if not permissions_values
or discord.Permissions.VALID_FLAGS[permission_name] in permissions_values
}
return base, permissions_dict
async def get_embeds(
self,
guild: discord.Guild,
roles: typing.List[discord.Role] = None,
members: typing.List[discord.Member] = None,
channel: typing.Optional[discord.abc.GuildChannel] = None,
permissions: typing.List[str] = None,
advanced: bool = False,
embed_color: discord.Color = discord.Color.green(),
) -> typing.List[discord.Embed]:
roles = [] if roles is None else roles.copy()
if members is None:
members = []
for member in members:
roles.extend(member.roles)
roles = sorted(set(roles))
if permissions is None:
permissions = []
embeds: typing.List[discord.Embed] = []
if not permissions or channel is not None:
__, permissions_dict = await self.get_permissions(
guild=guild, roles=roles, members=members, channel=channel, permissions=permissions
)
embed: discord.Embed = discord.Embed(
title=(_("Advanced ") if advanced else "") + _("View Permissions"),
color=embed_color,
)
embed.set_author(name=guild.name, icon_url=guild.icon)
embed.timestamp = datetime.datetime.now(tz=datetime.timezone.utc)
description = ""
if roles:
description += _("\n**Role(s):** {roles}").format(
roles=humanize_list([f"{role.mention} ({role.id})" for role in roles])
)
if members:
description += _("\n**Member(s):** {members}").format(
members=humanize_list(
[f"{member.mention} ({member.id})" for member in members]
)
)
if channel:
description += _("\n**Channel:** {channel}").format(
channel=f"{channel.mention} ({channel.id})"
)
if permissions:
description += _("\n**Permission(s) checked:** {permissions}").format(
permissions=humanize_list(
[f"`{permission_name}`" for permission_name in permissions]
)
)
embed.description = (
description if len(description) <= 4000 else f"{description[:3997]}..."
)
if not advanced:
e = embed.copy()
permissions_strings = "\n".join(
f"{'' if args['value'] else ''} {args['qualified_name']}"
for __, args in permissions_dict.items()
)
for page in pagify(permissions_strings, page_length=1024 // 3):
e.add_field(name="\u200c", value=page, inline=True)
embeds.append(e)
else:
max_len = (
max(len(args["qualified_name"]) for args in permissions_dict.values()) + 2
)
permissions_strings = "\n".join(
f"`{' ' * (max_len - len(args['qualified_name']) - 2)}{args['qualified_name']}` {'' if args['value'] else ''}{f' {source}' if (source := args['source']) is not None else ''}"
for __, args in permissions_dict.items()
)
embeds: typing.List[discord.Embed] = []
pages = list(pagify(permissions_strings, page_length=1024))
for i, page in enumerate(pages, start=1):
e = embed.copy()
e.add_field(name="\u200c", value=page, inline=True)
e.set_footer(text=f"Page {i}/{len(pages)}")
embeds.append(e)
else:
embed: discord.Embed = discord.Embed(title=_("View Permissions"), color=embed_color)
embed.set_author(name=guild.name, icon_url=guild.icon)
embed.timestamp = datetime.datetime.now(tz=datetime.timezone.utc)
description = ""
if roles:
description += _("\n**Role(s):** {roles}").format(
roles=humanize_list([f"{role.mention} ({role.id})" for role in roles])
)
if members:
description += _("\n**Member(s):** {members}").format(
members=humanize_list(
[f"{member.mention} ({member.id})" for member in members]
)
)
if permissions:
description += _("\n**Permission(s) checked:** {permissions}").format(
permissions=humanize_list(
[f"`{permission_name}`" for permission_name in permissions]
)
)
embed.description = (
description if len(description) <= 4000 else f"{description[:3997]}..."
)
_description = []
channels = sorted(
[channel for channel in guild.text_channels if channel.category is None],
key=lambda channel: channel.position,
)
channels.extend(
sorted(
[channel for channel in guild.voice_channels if channel.category is None],
key=lambda channel: channel.position,
)
)
for category in guild.categories:
if not category.channels:
continue
channels.append(category)
channels.extend(
sorted(
category.channels,
key=lambda channel: (
1 if channel.type == discord.ChannelType.voice else 0,
channel.position,
),
)
)
for channel in channels:
if isinstance(channel, discord.CategoryChannel):
_description.append(f"\n**{channel.name.upper()}:**")
continue
__, permissions_dict = await self.get_permissions(
guild=guild,
roles=roles,
members=members,
channel=channel,
permissions=permissions,
)
_description.append(
f"{'' if all(value['value'] for value in permissions_dict.values()) else ''} {channel.mention} ({channel.id})"
)
description = "\n".join(_description)
embeds: typing.List[discord.Embed] = []
pages = list(pagify(description, page_length=1024))
for i, page in enumerate(pages, start=1):
e = embed.copy()
e.add_field(name="\u200c", value=page, inline=True)
e.set_footer(text=f"Page {i}/{len(pages)}")
embeds.append(e)
return embeds
@commands.guild_only()
@commands.bot_has_permissions(embed_links=True)
@commands.hybrid_command(aliases=["viewperms", "permsview"])
async def viewpermissions(
self,
ctx: commands.Context,
advanced: typing.Optional[bool] = False,
channel: typing.Optional[discord.abc.GuildChannel] = None,
permissions: commands.Greedy[PermissionConverter] = None,
mentionables: commands.Greedy[typing.Union[discord.Role, discord.Member]] = None,
) -> None: # commands.CurrentChannel
"""Display permissions for roles and members, at guild level or in a specified channel.
- You can specify several roles and members, and their permissions will be added together.
- If you don't provide a channel, only permissions at the guild level will be displayed.
- If you provide permission(s) and a channel, only these permissions will be displayed for this channel.
- If you provide permission(s) and no channel, all guild channels will be displayed, with a tick if all the specified permissions are true in the channel.
- If you provide permission(s) and no mentionables, the everyone role is used.
"""
if permissions is None:
permissions = []
if mentionables is None:
mentionables = []
roles = [
mentionable for mentionable in mentionables if isinstance(mentionable, discord.Role)
]
members = [
mentionable for mentionable in mentionables if isinstance(mentionable, discord.Member)
]
for member in members:
roles.extend(member.roles)
await PermissionsView(
cog=self,
guild=ctx.guild,
roles=sorted(set(roles)),
members=members,
channel=getattr(channel, "parent", channel),
permissions=permissions,
advanced=advanced,
).start(ctx)