Compare commits

..

No commits in common. "main" and "mdiluz-patch-1" have entirely different histories.

7 changed files with 49 additions and 203 deletions

View file

@ -17,7 +17,7 @@ Matchy supports a bunch of user, `matcher` and bot owner commands. `/` commands
| /pause | user | Pauses the user for `days: int` days |
| /list | user | Lists the current matchees and scheduled matches |
| /match | user* | Shares a preview of the matchee groups of size `group_min: int` with the user. *Offers a button to post the match to `matcher` users |
| /schedule | `matcher` | Schedules a match every week with `group_min: int` users on `weekday: int` and at `hour: int`, on a weekly `cadence: int`.<br>Can pass `cancel: True` to stop the schedule |
| /schedule | `matcher` | Schedules a match every week with `group_min: int` users on `weekday: int` day and at `hour: int` hour. Can pass `cancel: True` to stop the schedule |
| /cancel | `matcher` | Cancels any scheduled matches in this channel |
| $sync | bot owner | Syncs bot command data with the discord servers |
| $close | bot owner | Closes the bot connection so the bot quits safely |

View file

@ -92,8 +92,8 @@ class MatcherCog(commands.Cog):
msg += "\n" + strings.paused_matchees(mentions) + "\n"
tasks = state.State.get_channel_match_tasks(interaction.channel.id)
for (day, hour, min, cadence, cadence_start) in tasks:
next_run = util.get_next_datetime_with_cadence(day, hour, datetime.now(), cadence, cadence_start)
for (day, hour, min) in tasks:
next_run = util.get_next_datetime(day, hour)
msg += "\n" + strings.scheduled(next_run, min)
if not msg:
@ -110,8 +110,7 @@ class MatcherCog(commands.Cog):
interaction: discord.Interaction,
members_min: int | None = None,
weekday: int | None = None,
hour: int | None = None,
cadence: int | None = None):
hour: int | None = None):
"""Schedule a match using the input parameters"""
# Set all the defaults
@ -121,8 +120,6 @@ class MatcherCog(commands.Cog):
weekday = 0
if hour is None:
hour = 9
if cadence is None or cadence == 0:
cadence = 1
channel_id = str(interaction.channel.id)
# Bail if not a matcher
@ -132,19 +129,19 @@ class MatcherCog(commands.Cog):
return
# Add the scheduled task and save
(_, _, _, _, cadence_start) = state.State.set_channel_match_task(
channel_id, members_min, weekday, hour, cadence)
state.State.set_channel_match_task(
channel_id, members_min, weekday, hour)
# Let the user know what happened
logger.info("Scheduled new match task in %s with min %s weekday %s hour %s and cadence %s",
channel_id, members_min, weekday, hour, cadence)
next_run = util.get_next_datetime_with_cadence(weekday, hour, datetime.now(), cadence, cadence_start)
logger.info("Scheduled new match task in %s with min %s weekday %s hour %s",
channel_id, members_min, weekday, hour)
next_run = util.get_next_datetime(weekday, hour)
view = discord.ui.View(timeout=None)
view.add_item(ScheduleButton())
await interaction.response.send_message(
strings.scheduled_success(next_run, cadence),
strings.scheduled_success(next_run),
ephemeral=True, silent=True, view=view)
@app_commands.command(description="Cancel all scheduled matches in this channel")
@ -210,28 +207,15 @@ class MatcherCog(commands.Cog):
async def run_hourly_tasks(self):
"""Run any hourly tasks we have"""
# Send a reminder for anything that will be active in 1 day
for (channel, _) in state.State.get_active_match_tasks(datetime.now() + timedelta(days=1)):
logger.info("Reminding about scheduled task in %s", channel)
msg_channel = self.bot.get_channel(int(channel))
await msg_channel.send(strings.reminder())
# Match groups for anything active right now
for (channel, min) in state.State.get_active_match_tasks():
logger.info("Scheduled match task triggered in %s", channel)
msg_channel = self.bot.get_channel(int(channel))
await match_groups_in_channel(msg_channel, min)
# Send a reminder to threads for a match that happened two days ago
for (channel, _) in state.State.get_active_match_tasks(datetime.now() - timedelta(days=2)):
logger.info("Sending reminders to threads in %s", channel)
for (channel, _) in state.State.get_active_match_tasks(datetime.now() + timedelta(days=1)):
logger.info("Reminding about scheduled task in %s", channel)
msg_channel = self.bot.get_channel(int(channel))
# Find any threads that need
for thread in msg_channel.threads:
# Only regard threads the bot created
# And that have no additional messages
if thread.owner.id == self.bot.user.id and thread.message_count <= 1:
await thread.send(strings.thread_reminder())
await msg_channel.send(strings.reminder())
# Increment when adjusting the custom_id so we don't confuse old users
@ -285,14 +269,11 @@ async def match_groups_in_channel(channel: discord.channel, min: int):
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:
thread = await channel.create_thread(
await channel.create_thread(
name=strings.thread_title([m.display_name for m in group]),
message=message,
reason="Creating a matching thread")
# Send a message with a suggested time to the channel
await thread.send(f"{strings.thread_message()} {strings.time_suggestion()}")
# Close off with a message
await channel.send(strings.matching_done())
# Save the groups to the history
@ -323,8 +304,8 @@ class ScheduleButton(discord.ui.Button):
msg += strings.scheduled_matches()
if tasks:
for (day, hour, min, cadence, cadence_start) in tasks:
next_run = util.get_next_datetime_with_cadence(day, hour, datetime.now(), cadence, cadence_start)
for (day, hour, min) in tasks:
next_run = util.get_next_datetime(day, hour)
msg += strings.scheduled(next_run, min)
await interaction.channel.send(msg)

View file

@ -85,11 +85,11 @@ def need_matcher_scope(): return [
@randomised
def scheduled_success(d, cadence): return [
f"Done :) Next run will be at {datetime_as_discord_time(d)} and every {cadence} week(s) after",
f"Woohoo! Scheduled for {datetime_as_discord_time(d)} plus each {cadence} week(s) after",
f"Yessir, will do a matcho every {cadence} week(s), first one is {datetime_as_discord_time(d)}",
f"Arf Arf! Bork bork bark {datetime_as_discord_time(d)} berk {cadence} week(s) arf",
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)}",
]
@ -145,16 +145,6 @@ Make sure you're /pause'd if you need to be, or /join in ASAP!""",
]
@randomised
def thread_reminder(): return [
"Hey friends, just checking in! No worries if you're too busy this week",
"Bork bork, quick reminder in case y'all forgot!",
"Hey matchees, how's your week going?",
"Hey everyone, don't forget to check in with eachother!",
"Quick friendly nudge, how're you all doing?",
]
@randomised
def matching(): return [
"Matchy is matching matchees...",
@ -184,36 +174,6 @@ def matched_up(ms): return [
]
@randomised
def thread_message(): return [
"Hey peeps :)",
"How is everyone?",
"Bork!",
"Hey kiddos :)",
"Ahoy!",
"Great to see y'all here.",
"Icebreaker! What's your favourite pokemon?",
"I'm hungry, would a lasagna count as a sandwich?",
"What's your favourite keyboard key?",
"I'm confused thinking... Is a train just a sideways elevator?",
"Humans are weird, why do you have moustaches above your eyes?"
]
@randomised
def time_suggestion(): return [
"Can I suggest a quick call on Wednesday?",
"Remember to organise a chat if you're up for it!",
"How about throwing something in the calendar for Friday?",
"Would 10am on Thursday work for people?",
"How about a call this afternoon?",
"Would a chat the start of a weekday work?",
"How's about organising a quick call sometime this week?",
"When's everyone available for a short hangout?",
"It's best to pick a time to drop in a call or meet up, if everyone is up for it!",
]
@randomised
def thread_title(ms): return [
f"{format_list(ms)}",

View file

@ -16,7 +16,7 @@ logger = logging.getLogger("state")
logger.setLevel(logging.INFO)
# Warning: Changing any of the below needs proper thought to ensure backwards compatibility
_VERSION = 5
_VERSION = 4
def _migrate_to_v1(d: dict):
@ -64,24 +64,12 @@ def _migrate_to_v4(d: dict):
del d[_Key._HISTORY]
def _migrate_to_v5(d: dict):
"""v5 added weekly cadence"""
tasks = d.get(_Key.TASKS, {})
for tasks in tasks.values():
match_tasks = tasks.get(_Key.MATCH_TASKS, [])
for match in match_tasks:
# All previous matches were every week starting from now
match[_Key.CADENCE] = 1
match[_Key.CADENCE_START] = datetime_to_ts(datetime.now())
# Set of migration functions to apply
_MIGRATIONS = [
_migrate_to_v1,
_migrate_to_v2,
_migrate_to_v3,
_migrate_to_v4,
_migrate_to_v5
]
@ -106,8 +94,6 @@ class _Key(str):
MEMBERS_MIN = "members_min"
WEEKDAY = "weekdays"
HOUR = "hours"
CADENCE = "cadence"
CADENCE_START = "cadence_start"
# Unused
_MATCHEES = "matchees"
@ -153,8 +139,6 @@ _SCHEMA = Schema(
_Key.MEMBERS_MIN: Use(int),
_Key.WEEKDAY: Use(int),
_Key.HOUR: Use(int),
_Key.CADENCE: Use(int),
_Key.CADENCE_START: Use(str),
}
]
}
@ -232,12 +216,11 @@ class _State():
@wraps(func)
def inner(self, *args, **kwargs):
tmp = _State(self._dict, self._file)
ret = func(tmp, *args, **kwargs)
func(tmp, *args, **kwargs)
_SCHEMA.validate(tmp._dict)
if tmp._file:
_save(tmp._file, tmp._dict)
self._dict = tmp._dict
return ret
return inner
@ -341,10 +324,7 @@ class _State():
for channel, tasks in self._tasks.items():
for match in tasks.get(_Key.MATCH_TASKS, []):
# Take into account the weekly cadence
start = ts_to_datetime(match[_Key.CADENCE_START])
weeks = int((time - start).days / 7)
if match[_Key.WEEKDAY] == weekday and match[_Key.HOUR] == hour and weeks % match[_Key.CADENCE] == 0:
if match[_Key.WEEKDAY] == weekday and match[_Key.HOUR] == hour:
yield (channel, match[_Key.MEMBERS_MIN])
def get_channel_match_tasks(self, channel_id: str) -> Generator[int, int, int]:
@ -358,35 +338,30 @@ class _State():
)
for tasks in all_tasks:
for task in tasks:
yield _task_to_tuple(task)
yield (task[_Key.WEEKDAY], task[_Key.HOUR], task[_Key.MEMBERS_MIN])
@safe_write
def set_channel_match_task(self, channel_id: str, members_min: int, weekday: int, hour: int, cadence: int):
def set_channel_match_task(self, channel_id: str, members_min: int, weekday: int, hour: int):
"""Set up a match task on a channel"""
channel = self._tasks.setdefault(str(channel_id), {})
matches = channel.setdefault(_Key.MATCH_TASKS, [])
for match_task in matches:
found = False
for match in matches:
# Specifically check for the combination of weekday and hour
if match_task[_Key.WEEKDAY] == weekday and match_task[_Key.HOUR] == hour:
match_task[_Key.MEMBERS_MIN] = members_min
# If the cadence has changed, update it and reset the start
if cadence != match_task[_Key.CADENCE]:
match_task[_Key.CADENCE] = cadence
match_task[_Key.CADENCE_START] = datetime_to_ts(datetime.now())
# Return as we've successfully changed the data in place
return _task_to_tuple(match_task)
if match[_Key.WEEKDAY] == weekday and match[_Key.HOUR] == hour:
found = True
match[_Key.MEMBERS_MIN] = members_min
# Return true as we've successfully changed the data in place
return True
# If we didn't find it, add it to the schedule
match_task = {
_Key.MEMBERS_MIN: members_min,
_Key.WEEKDAY: weekday,
_Key.HOUR: hour,
_Key.CADENCE: cadence,
_Key.CADENCE_START: datetime_to_ts(datetime.now())
}
matches.append(match_task)
return _task_to_tuple(match_task)
if not found:
matches.append({
_Key.MEMBERS_MIN: members_min,
_Key.WEEKDAY: weekday,
_Key.HOUR: hour,
})
@safe_write
def remove_channel_match_tasks(self, channel_id: str):
@ -404,14 +379,6 @@ class _State():
return self._dict[_Key.TASKS]
def _task_to_tuple(task):
return (task[_Key.WEEKDAY],
task[_Key.HOUR],
task[_Key.MEMBERS_MIN],
task[_Key.CADENCE],
ts_to_datetime(task[_Key.CADENCE_START]))
def load_from_file(file: str) -> _State:
"""
Load the state from a files

View file

@ -18,32 +18,20 @@ def format_list(list: list) -> str:
return list[0] if list else ''
def get_next_datetime(weekday: int, hour: int, start: datetime) -> datetime:
def get_next_datetime(weekday, hour) -> datetime:
"""Get the next datetime for the given weekday and hour"""
days_until_next_week_run = (weekday - start.weekday() + 7) % 7
now = datetime.now()
days_until_next_week = (weekday - now.weekday() + 7) % 7
# Account for when we're already beyond the time now
if days_until_next_week_run == 0 and start.hour >= hour:
days_until_next_week_run = 7
if days_until_next_week == 0 and now.hour >= hour:
days_until_next_week = 7
# Calculate the next datetime
next_date = start + timedelta(days=days_until_next_week_run)
next_date = now + timedelta(days=days_until_next_week)
next_date = next_date.replace(hour=hour, minute=0, second=0, microsecond=0)
return next_date.replace(hour=hour, minute=0, second=0, microsecond=0)
def get_next_datetime_with_cadence(weekday: int, hour: int, start: datetime, cadence: int, cadence_start: datetime):
"""Get the next datetime for given weekday, hour and cadence values"""
# Get the first run based on the cadence
next_time = get_next_datetime(weekday, hour, cadence_start)
# Walk forwards until we get the actual time
while next_time < start:
next_time += timedelta(weeks=cadence)
return next_time
return next_date
def datetime_as_discord_time(time: datetime) -> str:

View file

@ -1,8 +1,8 @@
coverage==7.6.10
coverage==7.6.1
discord.py==2.4.0
dpytest==0.7.0
flake8==7.1.1
pytest==8.3.3
pytest-asyncio==0.25.2
pytest-asyncio==0.24.0
pytest-cov==5.0.0
schema==0.7.7

View file

@ -1,6 +1,4 @@
import matchy.util as util
from datetime import datetime
import pytest
def test_iterate_all_shifts():
@ -52,51 +50,3 @@ def test_randomized():
assert util.randomised(string)() == "foo"
assert util.randomised(list)() in list()
@pytest.mark.parametrize(
"weekday, hour, start, expected",
[
pytest.param(
0, 0, datetime(2024, 9, 22),
datetime(2024, 9, 23), id="tomorrow"
),
pytest.param(
4, 16, datetime(2024, 9, 22),
datetime(2024, 9, 27, 16), id="complicated"
),
],
)
def test_get_next_datetime(weekday, hour, start, expected):
value = util.get_next_datetime(weekday, hour, start)
assert value == expected
@pytest.mark.parametrize(
"weekday, hour, start, cadence, cadence_start, expected",
[
pytest.param(
0, 0, datetime(2024, 9, 22),
1, datetime(2024, 9, 22),
datetime(2024, 9, 23), id="tomorrow"
),
pytest.param(
0, 0, datetime(2024, 9, 22),
2, datetime(2024, 9, 22),
datetime(2024, 9, 23), id="every-other"
),
pytest.param(
0, 0, datetime(2024, 9, 22),
2, datetime(2024, 9, 14),
datetime(2024, 9, 30), id="every-other-before"
),
pytest.param(
0, 0, datetime(2024, 9, 22),
3, datetime(2024, 9, 14),
datetime(2024, 10, 7), id="every-third"
),
],
)
def test_get_next_datetime_with_cadence(weekday, hour, start, expected, cadence, cadence_start):
value = util.get_next_datetime_with_cadence(weekday, hour, start, cadence, cadence_start)
assert value == expected