diff options
| author | 2021-01-24 19:20:36 +0200 | |
|---|---|---|
| committer | 2021-01-24 19:20:36 +0200 | |
| commit | 8ec2a4d2d8f506c5fe778a4eb30183f04c478fbc (patch) | |
| tree | fe77ef254d783efa4360d1440a369331d4ea6c61 | |
| parent | Merge pull request #1344 from ChrisLovering/new-env-tag (diff) | |
| parent | Merge branch 'master' into feat/info/server-cmd-enhancements (diff) | |
Merge pull request #1166 from python-discord/feat/info/server-cmd-enhancements
Enhance the !server command.
| -rw-r--r-- | bot/exts/info/information.py | 165 | ||||
| -rw-r--r-- | bot/utils/channel.py | 16 | 
2 files changed, 100 insertions, 81 deletions
| diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 9fb875925..619204e5d 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -2,13 +2,11 @@ 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  import fuzzywuzzy -from discord import ChannelType, Colour, Embed, Guild, Message, Role, Status -from discord.abc import GuildChannel +from discord import Colour, Embed, Guild, Message, Role  from discord.ext.commands import BucketType, Cog, Context, Paginator, command, group, has_any_role  from bot import constants @@ -17,18 +15,12 @@ from bot.bot import Bot  from bot.converters import FetchedMember  from bot.decorators import in_whitelist  from bot.pagination import LinePaginator -from bot.utils.channel import is_mod_channel +from bot.utils.channel import is_mod_channel, is_staff_channel  from bot.utils.checks import cooldown_with_role_bypass, has_no_roles_check, in_whitelist_check  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.""" @@ -37,47 +29,53 @@ 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_channel_type_counts(guild: Guild) -> DefaultDict[str, int]: +        """Return the total amounts of the various types of channels in `guild`.""" +        channel_counter = defaultdict(int) -    def get_staff_channel_count(self, guild: Guild) -> int: -        """ -        Get the number of channels that are staff-only. +        for channel in guild.channels: +            if is_staff_channel(channel): +                channel_counter["staff"] += 1 +            else: +                channel_counter[str(channel.type)] += 1 -        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? +        return channel_counter -        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 +    @staticmethod +    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} -            everyone_can_read = self.role_can_read(channel, guild.default_role) +    def get_extended_server_info(self) -> str: +        """Return additional server info only visible in moderation channels.""" +        talentpool_info = "" +        if cog := self.bot.get_cog("Talentpool"): +            talentpool_info = f"Nominated: {len(cog.watched_users)}\n" -            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 +        bb_info = "" +        if cog := self.bot.get_cog("Big Brother"): +            bb_info = f"BB-watched: {len(cog.watched_users)}\n" -        return len(channel_ids) +        defcon_info = "" +        if cog := self.bot.get_cog("Defcon"): +            defcon_status = "Enabled" if cog.enabled else "Disabled" +            defcon_days = cog.days.days if cog.enabled else "-" +            defcon_info = f"Defcon status: {defcon_status}\nDefcon days: {defcon_days}\n" -    @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}") +        python_general = self.bot.get_channel(constants.Channels.python_discussion) -        channel_type_list = sorted(channel_type_list) -        return "\n".join(channel_type_list) +        return textwrap.dedent(f""" +            {talentpool_info}\ +            {bb_info}\ +            {defcon_info}\ +            {python_general.mention} cooldown: {python_general.slowmode_delay}s +        """)      @has_any_role(*constants.STAFF_ROLES)      @command(name="roles") @@ -152,51 +150,56 @@ 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 is_mod_channel(ctx.channel): +            embed.add_field(name="Moderation:", value=self.get_extended_server_info()) +          await ctx.send(embed=embed)      @command(name="user", aliases=["user_info", "member", "member_info"]) diff --git a/bot/utils/channel.py b/bot/utils/channel.py index 0c072184c..72603c521 100644 --- a/bot/utils/channel.py +++ b/bot/utils/channel.py @@ -32,6 +32,22 @@ def is_mod_channel(channel: discord.TextChannel) -> bool:          return False +def is_staff_channel(channel: discord.TextChannel) -> bool: +    """True if `channel` is considered a staff channel.""" +    guild = bot.instance.get_guild(constants.Guild.id) + +    if channel.type is discord.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 +    ) + +  def is_in_category(channel: discord.TextChannel, category_id: int) -> bool:      """Return True if `channel` is within a category with `category_id`."""      return getattr(channel, "category_id", None) == category_id | 
