diff options
| -rw-r--r-- | bot/constants.py | 6 | ||||
| -rw-r--r-- | bot/exts/utils/jams.py | 171 | ||||
| -rw-r--r-- | config-default.yml | 5 | 
3 files changed, 111 insertions, 71 deletions
| diff --git a/bot/constants.py b/bot/constants.py index 885b5c822..f33c14798 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -400,6 +400,8 @@ class Categories(metaclass=YAMLGetter):      modmail: int      voice: int +    # 2021 Summer Code Jam +    summer_code_jam: int  class Channels(metaclass=YAMLGetter):      section = "guild" @@ -437,6 +439,7 @@ class Channels(metaclass=YAMLGetter):      discord_py: int      esoteric: int      voice_gate: int +    code_jam_planning: int      admins: int      admin_spam: int @@ -495,8 +498,10 @@ class Roles(metaclass=YAMLGetter):      admins: int      core_developers: int +    code_jam_event_team: int      devops: int      domain_leads: int +    events_lead: int      helpers: int      moderators: int      mod_team: int @@ -504,7 +509,6 @@ class Roles(metaclass=YAMLGetter):      project_leads: int      jammers: int -    team_leaders: int  class Guild(metaclass=YAMLGetter): diff --git a/bot/exts/utils/jams.py b/bot/exts/utils/jams.py index 98fbcb303..d45f9b57f 100644 --- a/bot/exts/utils/jams.py +++ b/bot/exts/utils/jams.py @@ -1,17 +1,19 @@ +import csv  import logging  import typing as t +from collections import defaultdict -from discord import CategoryChannel, Guild, Member, PermissionOverwrite, Role +import discord  from discord.ext import commands -from more_itertools import unique_everseen  from bot.bot import Bot -from bot.constants import Roles +from bot.constants import Categories, Channels, Emojis, Roles  log = logging.getLogger(__name__)  MAX_CHANNELS = 50  CATEGORY_NAME = "Code Jam" +TEAM_LEADERS_COLOUR = 0x11806a  class CodeJams(commands.Cog): @@ -20,39 +22,57 @@ class CodeJams(commands.Cog):      def __init__(self, bot: Bot):          self.bot = bot -    @commands.command() +    @commands.group()      @commands.has_any_role(Roles.admins) -    async def createteam(self, ctx: commands.Context, team_name: str, members: commands.Greedy[Member]) -> None: +    async def codejam(self, ctx: commands.Context) -> None: +        """A Group of commands for managing Code Jams.""" +        if ctx.invoked_subcommand is None: +            await ctx.send_help(ctx.command) + +    @codejam.command() +    async def create(self, ctx: commands.Context, csv_file: t.Optional[str]) -> None:          """ -        Create team channels (voice and text) in the Code Jams category, assign roles, and add overwrites for the team. +        Create code-jam teams from a CSV file or a link to one, specifying the team names, leaders and members. + +        The CSV file must have 3 columns: 'Team Name', 'Team Member Discord ID', and 'Team Leader'. -        The first user passed will always be the team leader. +        This will create the text channels for the teams, and give the team leaders their roles.          """ -        # Ignore duplicate members -        members = list(unique_everseen(members)) - -        # We had a little issue during Code Jam 4 here, the greedy converter did it's job -        # and ignored anything which wasn't a valid argument which left us with teams of -        # two members or at some times even 1 member. This fixes that by checking that there -        # are always 3 members in the members list. -        if len(members) < 3: -            await ctx.send( -                ":no_entry_sign: One of your arguments was invalid\n" -                f"There must be a minimum of 3 valid members in your team. Found: {len(members)}" -                " members" -            ) -            return +        async with ctx.typing(): +            if csv_file: +                async with self.bot.http_session.get(csv_file) as response: +                    if response.status != 200: +                        await ctx.send(f"Got a bad response from the URL: {response.status}") +                        return -        team_channel = await self.create_channels(ctx.guild, team_name, members) -        await self.add_roles(ctx.guild, members) +                    csv_file = await response.text() -        await ctx.send( -            f":ok_hand: Team created: {team_channel}\n" -            f"**Team Leader:** {members[0].mention}\n" -            f"**Team Members:** {' '.join(member.mention for member in members[1:])}" -        ) +            elif ctx.message.attachments: +                csv_file = (await ctx.message.attachments[0].read()).decode("utf8") +            else: +                raise commands.BadArgument("You must include either a CSV file or a link to one.") + +            teams = defaultdict(list) +            reader = csv.DictReader(csv_file.splitlines()) + +            for row in reader: +                member = ctx.guild.get_member(int(row["Team Member Discord ID"])) + +                if member is None: +                    log.trace(f"Got an invalid member ID: {row['Team Member Discord ID']}") +                    continue + +                teams[row["Team Name"]].append((member, row["Team Leader"].upper() == "Y")) + +            team_leaders = await ctx.guild.create_role(name="Team Leaders", colour=TEAM_LEADERS_COLOUR) + +            for team_name, members in teams.items(): +                await self.create_team_channel(ctx.guild, team_name, members, team_leaders) -    async def get_category(self, guild: Guild) -> CategoryChannel: +            await self.create_team_leader_channel(ctx.guild, team_leaders) +            await ctx.send(f"{Emojis.check_mark} Created Code Jam with {len(teams)} teams.") + +    async def get_category(self, guild: discord.Guild) -> discord.CategoryChannel:          """          Return a code jam category. @@ -60,84 +80,97 @@ class CodeJams(commands.Cog):          """          for category in guild.categories:              # Need 2 available spaces: one for the text channel and one for voice. -            if category.name == CATEGORY_NAME and MAX_CHANNELS - len(category.channels) >= 2: +            if category.name == CATEGORY_NAME and len(category.channels) < MAX_CHANNELS:                  return category          return await self.create_category(guild) -    @staticmethod -    async def create_category(guild: Guild) -> CategoryChannel: +    async def create_category(self, guild: discord.Guild) -> discord.CategoryChannel:          """Create a new code jam category and return it."""          log.info("Creating a new code jam category.")          category_overwrites = { -            guild.default_role: PermissionOverwrite(read_messages=False), -            guild.me: PermissionOverwrite(read_messages=True) +            guild.default_role: discord.PermissionOverwrite(read_messages=False), +            guild.me: discord.PermissionOverwrite(read_messages=True)          } -        return await guild.create_category_channel( +        category = await guild.create_category_channel(              CATEGORY_NAME,              overwrites=category_overwrites,              reason="It's code jam time!"          ) +        await self.send_status_update( +            guild, f"Created a new category with the ID {category.id} for this Code Jam's team channels." +        ) + +        return category +      @staticmethod -    def get_overwrites(members: t.List[Member], guild: Guild) -> t.Dict[t.Union[Member, Role], PermissionOverwrite]: +    def get_overwrites( +        members: list[tuple[discord.Member, bool]], +        guild: discord.Guild, +    ) -> dict[t.Union[discord.Member, discord.Role], discord.PermissionOverwrite]:          """Get code jam team channels permission overwrites.""" -        # First member is always the team leader          team_channel_overwrites = { -            members[0]: PermissionOverwrite( -                manage_messages=True, -                read_messages=True, -                manage_webhooks=True, -                connect=True -            ), -            guild.default_role: PermissionOverwrite(read_messages=False, connect=False), +            guild.default_role: discord.PermissionOverwrite(read_messages=False), +            guild.get_role(Roles.moderators): discord.PermissionOverwrite(read_messages=True), +            guild.get_role(Roles.code_jam_event_team): discord.PermissionOverwrite(read_messages=True)          } -        # Rest of members should just have read_messages -        for member in members[1:]: -            team_channel_overwrites[member] = PermissionOverwrite( -                read_messages=True, -                connect=True +        for member, _ in members: +            team_channel_overwrites[member] = discord.PermissionOverwrite( +                read_messages=True              )          return team_channel_overwrites -    async def create_channels(self, guild: Guild, team_name: str, members: t.List[Member]) -> str: -        """Create team text and voice channels. Return the mention for the text channel.""" +    async def create_team_channel( +        self, +        guild: discord.Guild, +        team_name: str, +        members: list[tuple[discord.Member, bool]], +        team_leaders: discord.Role +    ) -> None: +        """Create the team's text channel.""" +        await self.add_team_leader_roles(members, team_leaders) +          # Get permission overwrites and category          team_channel_overwrites = self.get_overwrites(members, guild)          code_jam_category = await self.get_category(guild)          # Create a text channel for the team -        team_channel = await guild.create_text_channel( +        await code_jam_category.create_text_channel(              team_name,              overwrites=team_channel_overwrites, -            category=code_jam_category          ) -        # Create a voice channel for the team -        team_voice_name = " ".join(team_name.split("-")).title() +    async def create_team_leader_channel(self, guild: discord.Guild, team_leaders: discord.Role) -> None: +        """Create the Team Leader Chat channel for the Code Jam team leaders.""" +        category: discord.CategoryChannel = guild.get_channel(Categories.summer_code_jam) -        await guild.create_voice_channel( -            team_voice_name, -            overwrites=team_channel_overwrites, -            category=code_jam_category +        team_leaders_chat = await category.create_text_channel( +            name="team-leaders-chat", +            overwrites={ +                guild.default_role: discord.PermissionOverwrite(read_messages=False), +                team_leaders: discord.PermissionOverwrite(read_messages=True) +            }          ) -        return team_channel.mention +        await self.send_status_update(guild, f"Created {team_leaders_chat.mention} in the {category} category.") + +    async def send_status_update(self, guild: discord.Guild, message: str) -> None: +        """Inform the events lead with a status update when the command is ran.""" +        channel: discord.TextChannel = guild.get_channel(Channels.code_jam_planning) + +        await channel.send(f"<@&{Roles.events_lead}>\n\n{message}")      @staticmethod -    async def add_roles(guild: Guild, members: t.List[Member]) -> None: -        """Assign team leader and jammer roles.""" -        # Assign team leader role -        await members[0].add_roles(guild.get_role(Roles.team_leaders)) - -        # Assign rest of roles -        jammer_role = guild.get_role(Roles.jammers) -        for member in members: -            await member.add_roles(jammer_role) +    async def add_team_leader_roles(members: list[tuple[discord.Member, bool]], team_leaders: discord.Role) -> None: +        """Assign team leader role, the jammer role and their team role.""" +        for member, is_leader in members: +            if is_leader: +                await member.add_roles(team_leaders)  def setup(bot: Bot) -> None: diff --git a/config-default.yml b/config-default.yml index 394c51c26..8c30ecf69 100644 --- a/config-default.yml +++ b/config-default.yml @@ -142,6 +142,7 @@ guild:          moderators:         &MODS_CATEGORY  749736277464842262          modmail:            &MODMAIL        714494672835444826          voice:                              356013253765234688 +        summer_code_jam:                    861692638540857384      channels:          # Public announcement and news channels @@ -185,6 +186,7 @@ guild:          bot_commands:       &BOT_CMD        267659945086812160          esoteric:                           470884583684964352          voice_gate:                         764802555427029012 +        code_jam_planning:                  490217981872177157          # Staff          admins:             &ADMINS         365960823622991872 @@ -258,8 +260,10 @@ guild:          # Staff          admins:             &ADMINS_ROLE    267628507062992896          core_developers:                    587606783669829632 +        code_jam_event_team:                787816728474288181          devops:                             409416496733880320          domain_leads:                       807415650778742785 +        events_lead:                        778361735739998228          helpers:            &HELPERS_ROLE   267630620367257601          moderators:         &MODS_ROLE      831776746206265384          mod_team:           &MOD_TEAM_ROLE  267629731250176001 @@ -268,7 +272,6 @@ guild:          # Code Jam          jammers:        737249140966162473 -        team_leaders:   737250302834638889          # Streaming          video:          764245844798079016 | 
