aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/cogs/information.py83
-rw-r--r--tests/bot/cogs/test_information.py12
2 files changed, 85 insertions, 10 deletions
diff --git a/bot/cogs/information.py b/bot/cogs/information.py
index f0eb3a1ea..d3a2768d4 100644
--- a/bot/cogs/information.py
+++ b/bot/cogs/information.py
@@ -1,11 +1,13 @@
import colorsys
+import functools
import logging
import pprint
import textwrap
from collections import Counter, defaultdict
from string import Template
-from typing import Any, Mapping, Optional, Union
+from typing import Any, List, Mapping, Optional, Union
+import more_itertools
from discord import Colour, Embed, Member, Message, Role, Status, utils
from discord.ext.commands import BucketType, Cog, Context, Paginator, command, group
from discord.utils import escape_markdown
@@ -26,6 +28,61 @@ class Information(Cog):
def __init__(self, bot: Bot):
self.bot = bot
+ @staticmethod
+ def _get_channels_with_role_permission(ctx: Context, role: Role, perm: str, value: Optional[bool]) -> List[int]:
+ """Get a list of channel IDs where one of the specified roles can read."""
+ channel_ids = []
+
+ for channel in ctx.guild.channels:
+ overwrites = channel.overwrites_for(role)
+ if overwrites.is_empty():
+ continue
+
+ for _perm, _value in overwrites:
+ if _perm == perm and _value is value:
+ channel_ids.append(channel.id)
+
+ return channel_ids
+
+ _get_channels_where_role_can_read = functools.partialmethod(
+ _get_channels_with_role_permission,
+ perm='read_messages',
+ value=True
+ )
+
+ _get_channels_where_role_cannot_read = functools.partialmethod(
+ _get_channels_with_role_permission,
+ perm='read_messages',
+ value=False
+ )
+
+ def _get_staff_channel_count(self, ctx: Context) -> int:
+ """
+ Get 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.
+ """
+ helpers = ctx.guild.get_role(constants.Roles.helpers)
+ moderators = ctx.guild.get_role(constants.Roles.moderators)
+ admins = ctx.guild.get_role(constants.Roles.admins)
+ everyone = ctx.guild.default_role
+
+ # Let's build some lists of channels.
+ everyone_denied = self._get_channels_where_role_cannot_read(ctx, everyone)
+ staff_allowed = more_itertools.flatten([
+ self._get_channels_where_role_can_read(ctx, admins), # Admins has explicit read message allow
+ self._get_channels_where_role_can_read(ctx, moderators), # Moderators has explicit read message allow
+ self._get_channels_where_role_can_read(ctx, helpers), # Helpers has explicit read message allow
+ ])
+
+ # Now we need to check which channels are both denied for @everyone and permitted for staff
+ staff_channels = set(cid for cid in staff_allowed if cid in everyone_denied)
+ return len(staff_channels)
+
@with_role(*constants.MODERATION_ROLES)
@command(name="roles")
async def roles_info(self, ctx: Context) -> None:
@@ -104,13 +161,23 @@ class Information(Cog):
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_counter = Counter(c.type for c in ctx.guild.channels)
+ channel_type_list = []
+ for channel in channel_counter:
+ channel_type = str(channel).title()
+ channel_type_list.append(f"{channel_type} channels: {channel_counter[channel]}")
+
+ channel_type_list = sorted(channel_type_list)
+ channel_counts = "\n".join(channel_type_list).strip()
# 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)
+
# 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 +189,16 @@ class Information(Cog):
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]:,}
diff --git a/tests/bot/cogs/test_information.py b/tests/bot/cogs/test_information.py
index aca6b594f..79c0e0ad3 100644
--- a/tests/bot/cogs/test_information.py
+++ b/tests/bot/cogs/test_information.py
@@ -148,14 +148,18 @@ class InformationCogTests(unittest.TestCase):
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