diff --git a/.gitignore b/.gitignore index cf9809a..3995065 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ __pycache__ -config.py +config.json diff --git a/config.py b/config.py new file mode 100644 index 0000000..d9ffe28 --- /dev/null +++ b/config.py @@ -0,0 +1,9 @@ +"""Very simple config loading library""" +import json + +CONFIG = "config.json" + + +def load() -> dict: + with open(CONFIG) as f: + return json.load(f) diff --git a/matching.py b/matching.py new file mode 100644 index 0000000..a35f863 --- /dev/null +++ b/matching.py @@ -0,0 +1,59 @@ +"""Utility functions for matchy""" +import json +import random +from typing import Protocol + +CONFIG = "config.json" + + +def load(file: str) -> dict: + """Load a json file directly as a dict""" + with open(file) as f: + return json.load(f) + + +def load_config() -> dict: + """Load the current config into a dict""" + return load(CONFIG) + + +def objects_to_groups(matchees: list[object], + per_group: int) -> list[list[object]]: + """Generate the groups from the set of matchees""" + random.shuffle(matchees) + num_groups = max(len(matchees)//per_group, 1) + return [matchees[i::num_groups] for i in range(num_groups)] + + +class Member(Protocol): + """Protocol for the type of Member""" + @property + def mention(self) -> str: + pass + + +def group_to_message(group: list[Member]) -> str: + """Get the message to send for each group""" + mentions = [m.mention for m in group] + if len(group) > 1: + mentions = f"{', '.join(mentions[:-1])} and {mentions[-1]}" + else: + mentions = mentions[0] + return f"Matched up {mentions}!" + + +class Role(Protocol): + @property + def name(self) -> str: + pass + + +class Guild(Protocol): + @property + def roles(self) -> list[Role]: + pass + + +def get_role_from_guild(guild: Guild, role: str) -> Role: + """Find a role in a guild""" + return next((r for r in guild.roles if r.name == role), None) diff --git a/matchy_test.py b/matching_test.py similarity index 88% rename from matchy_test.py rename to matching_test.py index 14ad910..634db85 100644 --- a/matchy_test.py +++ b/matching_test.py @@ -3,7 +3,7 @@ """ import discord import pytest -import matchy +import matching @pytest.mark.parametrize("matchees, per_group", [ @@ -14,7 +14,7 @@ import matchy ]) def test_matchees_to_groups(matchees, per_group): """Test simple group matching works""" - groups = matchy.matchees_to_groups(matchees, per_group) + groups = matching.objects_to_groups(matchees, per_group) for group in groups: # Ensure the group contains the right number of members assert len(group) >= per_group diff --git a/matchy.py b/matchy.py index f67c7ad..e18f292 100755 --- a/matchy.py +++ b/matchy.py @@ -1,17 +1,17 @@ """ matchy.py - Discord bot that matches people into groups """ -import random import logging import importlib import discord from discord import app_commands from discord.ext import commands +import matching + # Config contains # TOKEN : str - Discord bot token # OWNERS : list[int] - ids of owners able to use the owner commands -if importlib.util.find_spec("config"): - import config +config = matching.load_config() logger = logging.getLogger("matchy") logger.setLevel(logging.INFO) @@ -33,7 +33,7 @@ async def on_ready(): @bot.command() @commands.dm_only() -@commands.check(lambda ctx: ctx.message.author.id in config.OWNERS) +@commands.check(lambda ctx: ctx.message.author.id in config["OWNERS"]) async def sync(ctx: commands.Context): """Handle sync command""" msg = await ctx.reply("Reloading config...", ephemeral=True) @@ -49,7 +49,7 @@ async def sync(ctx: commands.Context): @bot.command() @commands.dm_only() -@commands.check(lambda ctx: ctx.message.author.id in config.OWNERS) +@commands.check(lambda ctx: ctx.message.author.id in config["OWNERS"]) async def close(ctx: commands.Context): """Handle restart command""" await ctx.reply("Closing bot...", ephemeral=True) @@ -76,9 +76,9 @@ async def match(interaction: discord.Interaction, group_min: int = None, matchee matchee_role = "Matchee" # Grab the roles and verify the given role - matcher = get_role_from_guild(interaction.guild, "Matcher") + matcher = matching.get_role_from_guild(interaction.guild, "Matcher") matcher = matcher and matcher in interaction.user.roles - matchee = get_role_from_guild(interaction.guild, matchee_role) + matchee = matching.get_role_from_guild(interaction.guild, matchee_role) if not matchee: await interaction.response.send_message(f"Server is missing '{matchee_role}' role :(", ephemeral=True) return @@ -86,10 +86,10 @@ async def match(interaction: discord.Interaction, group_min: int = None, matchee # Create our groups! matchees = list( m for m in interaction.channel.members if matchee in m.roles) - groups = matchees_to_groups(matchees, group_min) + groups = matching.objects_to_groups(matchees, group_min) # Post about all the groups with a button to send to the channel - msg = '\n'.join(group_to_message(g) for g in groups) + msg = '\n'.join(matching.group_to_message(g) for g in groups) if not matcher: # Let a non-matcher know why they don't have the button msg += f"\nYou'll need the {matcher.mention if matcher else 'Matcher'}" msg += " role to send this to the channel, sorry!" @@ -100,18 +100,6 @@ async def match(interaction: discord.Interaction, group_min: int = None, matchee len(matchees), len(groups)) -def get_role_from_guild(guild: discord.guild, role: str) -> discord.role: - """Find a role in a guild""" - return next((r for r in guild.roles if r.name == role), None) - - -async def send_groups_to_channel(channel: discord.channel, groups: list[list[discord.Member]]): - """Send the group messages to a channel""" - for msg in (group_to_message(g) for g in groups): - await channel.send(msg) - await channel.send("That's all folks, happy matching and remember - DFTBA!") - - class GroupMessageButton(discord.ui.View): """A button to press to send the groups to the channel""" @@ -122,28 +110,12 @@ class GroupMessageButton(discord.ui.View): @discord.ui.button(label="Send groups to channel", style=discord.ButtonStyle.green, emoji="📮") async def send_to_channel(self, interaction: discord.Interaction, _button: discord.ui.Button): """Send the groups to the channel with the button is pressed""" - await send_groups_to_channel(interaction.channel, self.groups) + for msg in (matching.group_to_message(g) for g in self.groups): + await interaction.channel.send(msg) + await interaction.channel.send("That's all folks, happy matching and remember - DFTBA!") await interaction.response.edit_message(content="Groups sent to channel!", view=None) -def matchees_to_groups(matchees: list[discord.Member], - per_group: int) -> list[list[discord.Member]]: - """Generate the groups from the set of matchees""" - random.shuffle(matchees) - num_groups = max(len(matchees)//per_group, 1) - return [matchees[i::num_groups] for i in range(num_groups)] - - -def group_to_message(group: list[discord.Member]) -> str: - """Get the message to send for each group""" - mentions = [m.mention for m in group] - if len(group) > 1: - mentions = f"{', '.join(mentions[:-1])} and {mentions[-1]}" - else: - mentions = mentions[0] - return f"Matched up {mentions}!" - - if __name__ == "__main__": handler = logging.StreamHandler() - bot.run(config.TOKEN, log_handler=handler, root_logger=True) + bot.run(config["TOKEN"], log_handler=handler, root_logger=True)