diff options
| -rw-r--r-- | bot/exts/info/information.py | 179 |
1 files changed, 98 insertions, 81 deletions
diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 38e760ee3..73a377bf0 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -2,11 +2,10 @@ import colorsys import logging import pprint import textwrap -from collections import Counter, defaultdict -from string import Template -from typing import Any, Mapping, Optional, Tuple, Union +from collections import defaultdict +from typing import Any, DefaultDict, Dict, Mapping, Optional, Tuple, Union -from discord import ChannelType, Colour, Embed, Guild, Message, Role, Status, utils +from discord import ChannelType, Colour, Embed, Guild, Message, Role, utils from discord.abc import GuildChannel from discord.ext.commands import BucketType, Cog, Context, Paginator, command, group, has_any_role @@ -22,12 +21,6 @@ from bot.utils.time import time_since log = logging.getLogger(__name__) -STATUS_EMOTES = { - Status.offline: constants.Emojis.status_offline, - Status.dnd: constants.Emojis.status_dnd, - Status.idle: constants.Emojis.status_idle -} - class Information(Cog): """A cog with commands for generating embeds with server info, such as server stats and user info.""" @@ -36,47 +29,60 @@ class Information(Cog): self.bot = bot @staticmethod - def role_can_read(channel: GuildChannel, role: Role) -> bool: - """Return True if `role` can read messages in `channel`.""" - overwrites = channel.overwrites_for(role) - return overwrites.read_messages is True - - def get_staff_channel_count(self, guild: Guild) -> int: - """ - Get the number of channels that are staff-only. + def is_staff_channel(guild: Guild, channel: GuildChannel) -> bool: + """Determines if a given channel is staff-only.""" + if channel.type is ChannelType.category: + return False + + # Channel is staff-only if staff have explicit read allow perms + # and @everyone has explicit read deny perms + return any( + channel.overwrites_for(guild.get_role(staff_role)).read_messages is True + and channel.overwrites_for(guild.default_role).read_messages is False + for staff_role in constants.STAFF_ROLES + ) - We need to know two things about a channel: - - Does the @everyone role have explicit read deny permissions? - - Do staff roles have explicit read allow permissions? + @staticmethod + def get_channel_type_counts(guild: Guild) -> DefaultDict[str, int]: + """Return the total amounts of the various types of channels in `guild`.""" + channel_counter = defaultdict(int) - If the answer to both of these questions is yes, it's a staff channel. - """ - channel_ids = set() for channel in guild.channels: - if channel.type is ChannelType.category: - continue - - everyone_can_read = self.role_can_read(channel, guild.default_role) - - for role in constants.STAFF_ROLES: - role_can_read = self.role_can_read(channel, guild.get_role(role)) - if role_can_read and not everyone_can_read: - channel_ids.add(channel.id) - break + if Information.is_staff_channel(guild, channel): + channel_counter["staff"] += 1 + else: + channel_counter[str(channel.type)] += 1 - return len(channel_ids) + return channel_counter @staticmethod - def get_channel_type_counts(guild: Guild) -> str: - """Return the total amounts of the various types of channels in `guild`.""" - channel_counter = Counter(c.type for c in guild.channels) - channel_type_list = [] - for channel, count in channel_counter.items(): - channel_type = str(channel).title() - channel_type_list.append(f"{channel_type} channels: {count}") - - channel_type_list = sorted(channel_type_list) - return "\n".join(channel_type_list) + def get_member_counts(guild: Guild) -> Dict[str, int]: + """Return the total number of members for certain roles in `guild`.""" + roles = ( + guild.get_role(role_id) for role_id in ( + constants.Roles.helpers, constants.Roles.moderators, constants.Roles.admins, + constants.Roles.owners, constants.Roles.contributors, + ) + ) + return {role.name.title(): len(role.members) for role in roles} + + def get_extended_server_info(self, guild: Guild) -> str: + """Return additional server info only visible in moderation channels.""" + talentpool_count = len(self.bot.get_cog("Talentpool").watched_users) + bb_count = len(self.bot.get_cog("Big Brother").watched_users) + + defcon_cog = self.bot.get_cog("Defcon") + defcon_status = "Enabled" if defcon_cog.enabled else "Disabled" + defcon_days = defcon_cog.days.days if defcon_cog.enabled else "-" + python_general = self.bot.get_channel(constants.Channels.python_discussion) + + return textwrap.dedent(f""" + Nominated: {talentpool_count} + BB-watched: {bb_count} + Defcon status: {defcon_status} + Defcon days: {defcon_days} + {python_general.mention} cooldown: {python_general.slowmode_delay}s + """) @has_any_role(*constants.STAFF_ROLES) @command(name="roles") @@ -145,51 +151,62 @@ class Information(Cog): @command(name="server", aliases=["server_info", "guild", "guild_info"]) async def server_info(self, ctx: Context) -> None: """Returns an embed full of server information.""" + embed = Embed( + colour=Colour.blurple(), + title="Server Information", + ) + created = time_since(ctx.guild.created_at, precision="days") - features = ", ".join(ctx.guild.features) region = ctx.guild.region + num_roles = len(ctx.guild.roles) - 1 # Exclude @everyone - roles = len(ctx.guild.roles) - member_count = ctx.guild.member_count - channel_counts = self.get_channel_type_counts(ctx.guild) + # Server Features are only useful in certain channels + if ctx.channel.id in ( + *constants.MODERATION_CHANNELS, constants.Channels.dev_core, constants.Channels.dev_contrib + ): + features = f"\nFeatures: {', '.join(ctx.guild.features)}" + else: + features = "" - # How many of each user status? + # Member status py_invite = await self.bot.fetch_invite(constants.Guild.invite) online_presences = py_invite.approximate_presence_count offline_presences = py_invite.approximate_member_count - online_presences - embed = Embed(colour=Colour.blurple()) - - # How many staff members and staff channels do we have? - staff_member_count = len(ctx.guild.get_role(constants.Roles.helpers).members) - staff_channel_count = self.get_staff_channel_count(ctx.guild) - - # Because channel_counts lacks leading whitespace, it breaks the dedent if it's inserted directly by the - # f-string. While this is correctly formatted by Discord, it makes unit testing difficult. To keep the - # formatting without joining a tuple of strings we can use a Template string to insert the already-formatted - # channel_counts after the dedent is made. - embed.description = Template( - textwrap.dedent(f""" - **Server information** - Created: {created} - Voice region: {region} - Features: {features} - - **Channel counts** - $channel_counts - Staff channels: {staff_channel_count} - - **Member counts** - Members: {member_count:,} - Staff members: {staff_member_count} - Roles: {roles} - - **Member statuses** - {constants.Emojis.status_online} {online_presences:,} - {constants.Emojis.status_offline} {offline_presences:,} - """) - ).substitute({"channel_counts": channel_counts}) + member_status = ( + f"{constants.Emojis.status_online} {online_presences} " + f"{constants.Emojis.status_offline} {offline_presences}" + ) + + embed.description = textwrap.dedent(f""" + Created: {created} + Voice region: {region}{features} + Roles: {num_roles} + Member status: {member_status} + """) embed.set_thumbnail(url=ctx.guild.icon_url) + # Members + total_members = ctx.guild.member_count + member_counts = self.get_member_counts(ctx.guild) + member_info = "\n".join( + f"{role}: {count}" for role, count in member_counts.items() + ) + embed.add_field(name=f"Members: {total_members}", value=member_info) + + # Channels + total_channels = len(ctx.guild.channels) + channel_counts = self.get_channel_type_counts(ctx.guild) + channel_info = "\n".join( + f"{channel.title()}: {count}" for channel, count in sorted(channel_counts.items()) + ) + embed.add_field(name=f"Channels: {total_channels}", value=channel_info) + + # Additional info if ran in moderation channels + if ctx.channel.id in constants.MODERATION_CHANNELS: + embed.add_field( + name="Moderation:", value=self.get_extended_server_info(ctx.guild) + ) + await ctx.send(embed=embed) @command(name="user", aliases=["user_info", "member", "member_info"]) |