Repository: python-discord/bot

Issue:
Staff count for the !server embed
In Counts, we should have `Staff members` with the number of `Helpers`


Discussion:

Resolving patch:
diff --git a/bot/cogs/information.py b/bot/cogs/information.py
--- a/bot/cogs/information.py
+++ b/bot/cogs/information.py
@@ -6,7 +6,8 @@
 from string import Template
 from typing import Any, Mapping, Optional, Union
 
-from discord import Colour, Embed, Member, Message, Role, Status, utils
+from discord import ChannelType, Colour, Embed, Guild, Member, Message, Role, Status, utils
+from discord.abc import GuildChannel
 from discord.ext.commands import BucketType, Cog, Context, Paginator, command, group
 from discord.utils import escape_markdown
 
@@ -26,6 +27,49 @@ class Information(Cog):
     def __init__(self, bot: Bot):
         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.
+
+        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?
+
+        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
+
+        return len(channel_ids)
+
+    @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)
+
     @with_role(*constants.MODERATION_ROLES)
     @command(name="roles")
     async def roles_info(self, ctx: Context) -> None:
@@ -102,15 +146,16 @@ async def server_info(self, ctx: Context) -> None:
 
         roles = len(ctx.guild.roles)
         member_count = ctx.guild.member_count
-
-        # How many of each type of channel?
-        channels = Counter(c.type for c in ctx.guild.channels)
-        channel_counts = "".join(sorted(f"{str(ch).title()} channels: {channels[ch]}\n" for ch in channels)).strip()
+        channel_counts = self.get_channel_type_counts(ctx.guild)
 
         # How many of each user status?
         statuses = Counter(member.status for member in ctx.guild.members)
         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 formated 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
@@ -122,12 +167,16 @@ async def server_info(self, ctx: Context) -> None:
                 Voice region: {region}
                 Features: {features}
 
-                **Counts**
+                **Channel counts**
+                $channel_counts
+                Staff channels: {staff_channel_count}
+
+                **Member counts**
                 Members: {member_count:,}
+                Staff members: {staff_member_count}
                 Roles: {roles}
-                $channel_counts
 
-                **Members**
+                **Member statuses**
                 {constants.Emojis.status_online} {statuses[Status.online]:,}
                 {constants.Emojis.status_idle} {statuses[Status.idle]:,}
                 {constants.Emojis.status_dnd} {statuses[Status.dnd]:,}


Test patch:
diff --git a/tests/bot/cogs/test_information.py b/tests/bot/cogs/test_information.py
--- a/tests/bot/cogs/test_information.py
+++ b/tests/bot/cogs/test_information.py
@@ -148,14 +148,18 @@ def test_server_info_command(self, time_since_patch):
                 Voice region: {self.ctx.guild.region}
                 Features: {', '.join(self.ctx.guild.features)}
 
-                **Counts**
-                Members: {self.ctx.guild.member_count:,}
-                Roles: {len(self.ctx.guild.roles)}
+                **Channel counts**
                 Category channels: 1
                 Text channels: 1
                 Voice channels: 1
+                Staff channels: 0
+
+                **Member counts**
+                Members: {self.ctx.guild.member_count:,}
+                Staff members: 0
+                Roles: {len(self.ctx.guild.roles)}
 
-                **Members**
+                **Member statuses**
                 {constants.Emojis.status_online} 2
                 {constants.Emojis.status_idle} 1
                 {constants.Emojis.status_dnd} 4