Merge pull request #18 from mdiluz/character
Some checks failed
Test, Build and Publish / test (push) Has been cancelled
Test, Build and Publish / build-and-push-images (push) Has been cancelled

Add randomised strings to what matchy says
This commit is contained in:
Marc Di Luzio 2024-08-17 22:59:08 +01:00 committed by GitHub
commit dc14d48d20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 269 additions and 61 deletions

View file

@ -7,6 +7,5 @@
], ],
"python.testing.unittestEnabled": false, "python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true, "python.testing.pytestEnabled": true,
"autopep8.interpreter": [".venv/bin/python"],
"python.envFile": "${workspaceFolder}/.env", "python.envFile": "${workspaceFolder}/.env",
} }

View file

@ -12,6 +12,7 @@ import matchy.matching as matching
from matchy.state import AuthScope from matchy.state import AuthScope
import matchy.util as util import matchy.util as util
import matchy.state as state import matchy.state as state
import matchy.cogs.strings as strings
logger = logging.getLogger("cog") logger = logging.getLogger("cog")
@ -40,8 +41,8 @@ class MatcherCog(commands.Cog):
state.State.set_user_active_in_channel( state.State.set_user_active_in_channel(
interaction.user.id, interaction.channel.id) interaction.user.id, interaction.channel.id)
await interaction.response.send_message( await interaction.response.send_message(
f"Roger roger {interaction.user.mention}!\n" strings.acknowledgement(interaction.user.mention) + "\n"
+ f"Added you to {interaction.channel.mention}!", + strings.user_added(interaction.channel.mention),
ephemeral=True, silent=True) ephemeral=True, silent=True)
@app_commands.command(description="Leave the matchees for this channel") @app_commands.command(description="Leave the matchees for this channel")
@ -53,7 +54,7 @@ class MatcherCog(commands.Cog):
state.State.set_user_active_in_channel( state.State.set_user_active_in_channel(
interaction.user.id, interaction.channel.id, False) interaction.user.id, interaction.channel.id, False)
await interaction.response.send_message( await interaction.response.send_message(
f"No worries {interaction.user.mention}. Come back soon :)", ephemeral=True, silent=True) strings.user_leave(interaction.user.mention), ephemeral=True, silent=True)
@app_commands.command(description="Pause your matching in this channel for a number of days") @app_commands.command(description="Pause your matching in this channel for a number of days")
@commands.guild_only() @commands.guild_only()
@ -68,8 +69,8 @@ class MatcherCog(commands.Cog):
state.State.set_user_paused_in_channel( state.State.set_user_paused_in_channel(
interaction.user.id, interaction.channel.id, until) interaction.user.id, interaction.channel.id, until)
await interaction.response.send_message( await interaction.response.send_message(
f"Sure thing {interaction.user.mention}!\n" strings.acknowledgement(interaction.user.mention) + "\n"
+ f"Paused you until {util.datetime_as_discord_time(until)}!", + strings.paused(until),
ephemeral=True, silent=True) ephemeral=True, silent=True)
@app_commands.command(description="List the matchees for this channel") @app_commands.command(description="List the matchees for this channel")
@ -84,23 +85,19 @@ class MatcherCog(commands.Cog):
if matchees: if matchees:
mentions = [m.mention for m in matchees] mentions = [m.mention for m in matchees]
msg += f"There are {len(matchees)} active matchees:\n" msg += strings.active_matchees(mentions) + "\n"
msg += f"{util.format_list(mentions)}\n"
if paused: if paused:
mentions = [m.mention for m in paused] mentions = [m.mention for m in paused]
msg += f"\nThere are {len(mentions)} paused matchees:\n" msg += "\n" + strings.paused_matchees(mentions) + "\n"
msg += f"{util.format_list([m.mention for m in paused])}\n"
tasks = state.State.get_channel_match_tasks(interaction.channel.id) tasks = state.State.get_channel_match_tasks(interaction.channel.id)
for (day, hour, min) in tasks: for (day, hour, min) in tasks:
next_run = util.get_next_datetime(day, hour) next_run = util.get_next_datetime(day, hour)
date_str = util.datetime_as_discord_time(next_run) msg += "\n" + strings.scheduled(next_run, min)
msg += f"\nA match is scheduled at {date_str}"
msg += f" with {min} members per group\n"
if not msg: if not msg:
msg = "There are no matchees in this channel and no scheduled matches :(" msg = strings.no_scheduled()
await interaction.response.send_message(msg, ephemeral=True, silent=True) await interaction.response.send_message(msg, ephemeral=True, silent=True)
@ -127,7 +124,7 @@ class MatcherCog(commands.Cog):
# Bail if not a matcher # Bail if not a matcher
if not state.State.get_user_has_scope(interaction.user.id, AuthScope.MATCHER): if not state.State.get_user_has_scope(interaction.user.id, AuthScope.MATCHER):
await interaction.response.send_message("You'll need the 'matcher' scope to schedule a match", await interaction.response.send_message(strings.need_matcher_scope(),
ephemeral=True, silent=True) ephemeral=True, silent=True)
return return
@ -139,13 +136,12 @@ class MatcherCog(commands.Cog):
logger.info("Scheduled new match task in %s with min %s weekday %s hour %s", logger.info("Scheduled new match task in %s with min %s weekday %s hour %s",
channel_id, members_min, weekday, hour) channel_id, members_min, weekday, hour)
next_run = util.get_next_datetime(weekday, hour) next_run = util.get_next_datetime(weekday, hour)
date_str = util.datetime_as_discord_time(next_run)
view = discord.ui.View(timeout=None) view = discord.ui.View(timeout=None)
view.add_item(ScheduleButton()) view.add_item(ScheduleButton())
await interaction.response.send_message( await interaction.response.send_message(
f"Done :) Next run will be at {date_str}", strings.scheduled_success(next_run),
ephemeral=True, silent=True, view=view) ephemeral=True, silent=True, view=view)
@app_commands.command(description="Cancel all scheduled matches in this channel") @app_commands.command(description="Cancel all scheduled matches in this channel")
@ -154,7 +150,7 @@ class MatcherCog(commands.Cog):
"""Cancel scheduled matches in this channel""" """Cancel scheduled matches in this channel"""
# Bail if not a matcher # Bail if not a matcher
if not state.State.get_user_has_scope(interaction.user.id, AuthScope.MATCHER): if not state.State.get_user_has_scope(interaction.user.id, AuthScope.MATCHER):
await interaction.response.send_message("You'll need the 'matcher' scope to remove scheduled matches", await interaction.response.send_message(strings.need_matcher_scope(),
ephemeral=True, silent=True) ephemeral=True, silent=True)
return return
@ -163,8 +159,7 @@ class MatcherCog(commands.Cog):
state.State.remove_channel_match_tasks(channel_id) state.State.remove_channel_match_tasks(channel_id)
await interaction.response.send_message( await interaction.response.send_message(
"Done, all scheduled matches cleared in this channel!", strings.cancelled(), ephemeral=True, silent=True)
ephemeral=True, silent=True)
@app_commands.command(description="Match up matchees") @app_commands.command(description="Match up matchees")
@commands.guild_only() @commands.guild_only()
@ -186,24 +181,23 @@ class MatcherCog(commands.Cog):
# Let the user know when there's nobody to match # Let the user know when there's nobody to match
if not groups: if not groups:
await interaction.response.send_message("Nobody to match up :(", ephemeral=True, silent=True) await interaction.response.send_message(strings.nobody_to_match(), ephemeral=True, silent=True)
return return
# Post about all the groups with a button to send to the channel # Post about all the groups with a button to send to the channel
groups_list = '\n'.join( groups_list = '\n'.join(
", ".join([m.mention for m in g]) for g in groups) ", ".join([m.mention for m in g]) for g in groups)
msg = f"Roger! I've generated example groups for ya:\n\n{groups_list}" msg = strings.generated_groups(groups_list)
view = discord.utils.MISSING view = discord.utils.MISSING
if state.State.get_user_has_scope(interaction.user.id, AuthScope.MATCHER): if state.State.get_user_has_scope(interaction.user.id, AuthScope.MATCHER):
# Otherwise set up the button # Otherwise set up the button
msg += "\n\nClick the button to match up groups and send them to the channel.\n" msg += "\n\n" + strings.click_to_match() + "\n"
view = discord.ui.View(timeout=None) view = discord.ui.View(timeout=None)
view.add_item(MatchDynamicButton(members_min)) view.add_item(MatchDynamicButton(members_min))
else: else:
# Let a non-matcher know why they don't have the button # Let a non-matcher know why they don't have the button
msg += f"\n\nYou'll need the {AuthScope.MATCHER}" msg += "\n\n" + strings.need_matcher_to_post()
msg += " scope to post this to the channel, sorry!"
await interaction.response.send_message(msg, ephemeral=True, silent=True, view=view) await interaction.response.send_message(msg, ephemeral=True, silent=True, view=view)
@ -216,13 +210,12 @@ class MatcherCog(commands.Cog):
for (channel, min) in state.State.get_active_match_tasks(): for (channel, min) in state.State.get_active_match_tasks():
logger.info("Scheduled match task triggered in %s", channel) logger.info("Scheduled match task triggered in %s", channel)
msg_channel = self.bot.get_channel(int(channel)) msg_channel = self.bot.get_channel(int(channel))
await matching.match_groups_in_channel(state.State, msg_channel, min) await match_groups_in_channel(msg_channel, min)
for (channel, _) in state.State.get_active_match_tasks(datetime.now() + timedelta(days=1)): for (channel, _) in state.State.get_active_match_tasks(datetime.now() + timedelta(days=1)):
logger.info("Reminding about scheduled task in %s", channel) logger.info("Reminding about scheduled task in %s", channel)
msg_channel = self.bot.get_channel(int(channel)) msg_channel = self.bot.get_channel(int(channel))
await msg_channel.send("Arf arf! just a reminder I'll be doin a matcherino in here in T-24hrs!" await msg_channel.send(strings.reminder())
+ "\nUse /join if you haven't already, or /pause if you want to skip a week :)")
# Increment when adjusting the custom_id so we don't confuse old users # Increment when adjusting the custom_id so we don't confuse old users
@ -260,10 +253,33 @@ class MatchDynamicButton(discord.ui.DynamicItem[discord.ui.Button],
intrctn.guild.name, intrctn.channel.name) intrctn.guild.name, intrctn.channel.name)
# Let the user know we've recieved the message # Let the user know we've recieved the message
await intrctn.response.send_message(content="Matchy is matching matchees...", ephemeral=True) await intrctn.response.send_message(content=strings.matching(), ephemeral=True)
# Perform the match # Perform the match
await matching.match_groups_in_channel(intrctn.channel, self.min) await match_groups_in_channel(intrctn.channel, self.min)
async def match_groups_in_channel(channel: discord.channel, min: int):
"""Match up the groups in a given channel"""
groups = matching.active_members_to_groups(channel, min)
# Send the groups
for group in groups:
message = await channel.send(
strings.matched_up([m.mention for m in group]))
# Set up a thread for this match if the bot has permissions to do so
if channel.permissions_for(channel.guild.me).create_public_threads:
await channel.create_thread(
name=strings.thread_title([m.display_name for m in group]),
message=message,
reason="Creating a matching thread")
# Close off with a message
await channel.send(strings.matching_done())
# Save the groups to the history
state.State.log_groups(groups)
logger.info("Done! Matched into %s groups.", len(groups))
class ScheduleButton(discord.ui.Button): class ScheduleButton(discord.ui.Button):
@ -284,16 +300,16 @@ class ScheduleButton(discord.ui.Button):
tasks = state.State.get_channel_match_tasks(interaction.channel.id) tasks = state.State.get_channel_match_tasks(interaction.channel.id)
msg = f"{interaction.user.mention} added a match to this channel!\n" msg = strings.added_schedule(interaction.user.mention) + "\n"
msg += "Current scheduled matches are:" msg += strings.scheduled_matches()
if tasks: if tasks:
for (day, hour, min) in tasks: for (day, hour, min) in tasks:
next_run = util.get_next_datetime(day, hour) next_run = util.get_next_datetime(day, hour)
date_str = util.datetime_as_discord_time(next_run) msg += strings.scheduled(next_run, min)
msg += f"\n{date_str} with {min} members per group\n"
await interaction.channel.send(msg) await interaction.channel.send(msg)
await interaction.response.send_message(content="Posted :)", ephemeral=True) await interaction.response.send_message(
content=strings.acknowledgement(interaction.user.mention), ephemeral=True)
else: else:
await interaction.response.send_message(content="No scheduled matches to post :(", ephemeral=True) await interaction.response.send_message(content=strings.no_scheduled(), ephemeral=True)

View file

@ -51,4 +51,4 @@ class OwnerCog(commands.Cog):
logger.info("Granting user %s matcher scope", user) logger.info("Granting user %s matcher scope", user)
await ctx.reply("Done!", ephemeral=True) await ctx.reply("Done!", ephemeral=True)
else: else:
await ctx.reply("Likely not a user...", ephemeral=True) await ctx.reply(f"{user} is not a user?", ephemeral=True)

195
matchy/cogs/strings.py Normal file
View file

@ -0,0 +1,195 @@
"""
All the possible strings for things that matchy can say
Some can be selected randomly to give the bot some flavor
"""
from matchy.util import randomised, datetime_as_discord_time, format_list
from matchy.state import AuthScope
# Acknowledge something
@randomised
def acknowledgement(m): return [
f"Roger roger {m}!",
f"Sure thing {m}!",
f"o7 {m}!",
f"Yessir {m}!",
f"Bork {m}!",
]
@randomised
def user_added(c): return [
f"Added you to {c}!",
f"You've joined the matchee list on {c}!",
f"Awesome, great to have you on board in {c}!",
f"Bark bork bork arf {c} bork!",
]
@randomised
def user_leave(m): return [
f"No worries {m}. Come back soon :)",
f"That's cool {m}. Home you'll be back some day",
f"Cool cool {m}. Be well!",
f"Byeeee {m}!",
]
@randomised
def paused(t): return [
f"Paused you until {datetime_as_discord_time(t)}!",
f"You've been paused until {datetime_as_discord_time(t)}! Bork!",
f"Okie dokie you'll be unpaused at {datetime_as_discord_time(t)}!",
f"Bork bork bork arf {datetime_as_discord_time(t)}! Arf bork arf arf.",
]
@randomised
def active_matchees(ms): return [
f"There are {len(ms)} active matchees:\n{format_list(ms)}",
f"We've got {len(ms)} matchees here!\n{format_list(ms)}",
]
@randomised
def paused_matchees(ms): return [
f"There are {len(ms)} paused matchees:\n{format_list(ms)}",
f"Only {len(ms)} matchees are paused:\n{format_list(ms)}",
]
@randomised
def scheduled(next, n): return [
f"""A match is scheduled at {datetime_as_discord_time(next)} \
with {n} members per group""",
f"""There'll be a match at {datetime_as_discord_time(next)} \
with min {n} per group""",
f"""At {datetime_as_discord_time(next)} I'll match \
groups of minimum {n}""",
]
@randomised
def no_scheduled(): return [
"There are no matchees in this channel and no scheduled matches :(",
"This channel's got nothing, bork!",
"Arf bork no matchees or schedules here bark bork",
]
@randomised
def need_matcher_scope(): return [
f"You'll need the '{AuthScope.MATCHER}' scope to do this",
f"Only folks with the '{AuthScope.MATCHER}' scope can do this",
]
@randomised
def scheduled_success(d): return [
f"Done :) Next run will be at {datetime_as_discord_time(d)}",
f"Woohoo! Scheduled for {datetime_as_discord_time(d)}",
f"Yessir, will do a matcho at {datetime_as_discord_time(d)}",
f"Arf Arf! Bork bork bark {datetime_as_discord_time(d)}",
]
@randomised
def cancelled(): return [
"Done, all scheduled matches cleared in this channel!",
"See ya later schedulaters",
"Okie dokey, schedule cleared",
]
@randomised
def nobody_to_match(): return [
"Nobody to match up :(",
"Arf orf... no matchees found...",
"Couldn't find any matchees in this channel, sorry!",
]
@randomised
def generated_groups(g): return [
f"Roger! I've generated example groups for ya:\n\n{g}",
f"Sure thing! Some example groups:\n\n{g}",
f"Yessir! The groups might look like:\n\n{g}",
]
@randomised
def click_to_match(): return [
"Click the button to match up groups and send them to the channel.",
"Bonk the button to do a match.",
"Arf borf bork button bark press bork",
]
@randomised
def need_matcher_to_post(): return [
f"""You'll need the '{AuthScope.MATCHER}' \
scope to post this to the channel, sorry!""",
f"""You can't send this to the channel without \
the '{AuthScope.MATCHER}' scope.""",
]
@randomised
def reminder(): return [
"""Arf arf! just a reminder I'll be doin a matcherino in here in T-24hrs!
Use /join if you haven't already, or /pause if you want to skip a week :)""",
"""Bork! In T-24hrs there'll be a matchy match in this channel
Use /join to get in on the fun, or /pause need to take a break!""",
"""Woof! Your friendly neighbourhood matchy reminder here for tomorrow!
Make sure you're /pause'd if you need to be, or /join in ASAP!""",
]
@randomised
def matching(): return [
"Matchy is matching matchees...",
"Matchy is matching matchees!",
"Matchy is doin a match!",
"Matchy Match!",
]
@randomised
def matching_done(): return [
"That's all folks, happy matching and remember - DFTBA!",
"Aaand that's it, enjoy and vibe, you've earned it!",
"Until next time frendos.",
]
@randomised
def matched_up(ms): return [
f"Matched up {format_list(ms)}!",
f"Hey {format_list(ms)}, have a good match!",
f"Ahoy {format_list(ms)}, y'all be good :)",
f"Sweet! {format_list(ms)} are a group!",
f"Arf arf {format_list(ms)} bork bork arf!",
f"{format_list(ms)}, y'all are the best!",
f"{format_list(ms)}, DFTBA!",
]
@randomised
def thread_title(ms): return [
f"{format_list(ms)}",
]
@randomised
def added_schedule(m): return [
f"{m} added a match to this channel!"
f"A matchy run was scheduled by {m}."
f"{m} scheduled a match in here :)"
]
@randomised
def scheduled_matches(): return [
"Current scheduled matches are:"
"I've got these scheduled matches right now:"
]

View file

@ -177,29 +177,6 @@ def members_to_groups(matchees: list[Member],
assert False assert False
async def match_groups_in_channel(channel: discord.channel, min: int):
"""Match up the groups in a given channel"""
groups = active_members_to_groups(channel, min)
# Send the groups
for group in groups:
message = await channel.send(
f"Matched up {util.format_list([m.mention for m in group])}!")
# Set up a thread for this match if the bot has permissions to do so
if channel.permissions_for(channel.guild.me).create_public_threads:
await channel.create_thread(
name=util.format_list([m.display_name for m in group]),
message=message,
reason="Creating a matching thread")
# Close off with a message
await channel.send("That's all folks, happy matching and remember - DFTBA!")
# Save the groups to the history
state.State.log_groups(groups)
logger.info("Done! Matched into %s groups.", len(groups))
def get_matchees_in_channel(channel: discord.channel): def get_matchees_in_channel(channel: discord.channel):
"""Fetches the matchees in a channel""" """Fetches the matchees in a channel"""
# Reactivate any unpaused users # Reactivate any unpaused users
@ -215,6 +192,6 @@ def get_matchees_in_channel(channel: discord.channel):
def active_members_to_groups(channel: discord.channel, min_members: int): def active_members_to_groups(channel: discord.channel, min_members: int):
"""Helper to create groups from channel members""" """Helper to create groups from channel members"""
# Gather up the prospective matchees # Gather up the prospective matchees
matchees = get_matchees_in_channel(channel) (matchees, _) = get_matchees_in_channel(channel)
# Create our groups! # Create our groups!
return members_to_groups(matchees, min_members, allow_fallback=True) return members_to_groups(matchees, min_members, allow_fallback=True)

View file

@ -1,5 +1,6 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import reduce from functools import reduce
import random
def get_day_with_suffix(day): def get_day_with_suffix(day):
@ -59,3 +60,11 @@ def set_nested_value(d, *keys, value=None):
d[leaf] = value d[leaf] = value
elif leaf in d: elif leaf in d:
del d[leaf] del d[leaf]
def randomised(func):
"Randomise which in a list we actually return"
def wrapper(*args, **kwargs):
vals = func(*args, **kwargs)
return random.choice(vals) if isinstance(vals, list) else vals
return wrapper

View file

@ -38,3 +38,15 @@ def test_set_nested_dict_value():
} }
util.set_nested_value(d, "x", "y", "z", "val", value=52) util.set_nested_value(d, "x", "y", "z", "val", value=52)
assert 52 == util.get_nested_value(d, "x", "y", "z", "val") assert 52 == util.get_nested_value(d, "x", "y", "z", "val")
def test_randomized():
def string():
return "foo"
def list():
return ["foo", "bar"]
assert util.randomised(string)() == "foo"
assert util.randomised(list)() in list()