diff --git a/matchy/cogs/matcher.py b/matchy/cogs/matcher.py index fb1f0a0..dd6a91a 100644 --- a/matchy/cogs/matcher.py +++ b/matchy/cogs/matcher.py @@ -79,18 +79,30 @@ class MatcherCog(commands.Cog): logger.info("Handling /list command in %s %s from %s", interaction.guild.name, interaction.channel, interaction.user.name) - matchees = matching.get_matchees_in_channel( + (matchees, paused) = matching.get_matchees_in_channel( self.state, interaction.channel) - mentions = [m.mention for m in matchees] - msg = "Current matchees in this channel:\n" + \ - f"{util.format_list(mentions)}" + + msg = "" + + if matchees: + mentions = [m.mention for m in matchees] + msg += f"There are {len(matchees)} active matchees:\n" + msg += f"{util.format_list(mentions)}\n" + + if paused: + mentions = [m.mention for m in paused] + msg += f"\nThere are {len(mentions)} paused matchees:\n" + msg += f"{util.format_list([m.mention for m in paused])}\n" tasks = self.state.get_channel_match_tasks(interaction.channel.id) for (day, hour, min) in tasks: next_run = util.get_next_datetime(day, hour) date_str = util.datetime_as_discord_time(next_run) - msg += f"\nNext scheduled at {date_str}" - msg += f" with {min} members per group" + msg += f"\nA match is scheduled at {date_str}" + msg += f" with {min} members per group\n" + + if not msg: + msg = "There are no matchees in this channel and no scheduled matches :(" await interaction.response.send_message(msg, ephemeral=True, silent=True) diff --git a/matchy/matching.py b/matchy/matching.py index 4eaf66b..5c2403b 100644 --- a/matchy/matching.py +++ b/matchy/matching.py @@ -207,7 +207,9 @@ def get_matchees_in_channel(state: State, channel: discord.channel): # Reactivate any unpaused users state.reactivate_users(channel.id) # Gather up the prospective matchees - return [m for m in channel.members if state.get_user_active_in_channel(m.id, channel.id)] + active = [m for m in channel.members if state.get_user_active_in_channel(m.id, channel.id)] + paused = [m for m in channel.members if state.get_user_paused_in_channel(m.id, channel.id)] + return (active, paused) def active_members_to_groups(state: State, channel: discord.channel, min_members: int): diff --git a/matchy/state.py b/matchy/state.py index 00a0ce0..8803d6b 100644 --- a/matchy/state.py +++ b/matchy/state.py @@ -10,6 +10,7 @@ import pathlib import copy import logging from functools import wraps +import matchy.util as util logger = logging.getLogger("state") logger.setLevel(logging.INFO) @@ -275,40 +276,42 @@ class State(): Check if a user has an auth scope "owner" users have all scopes """ - user = self._users.get(str(id), {}) - scopes = user.get(_Key.SCOPES, []) + scopes = util.get_nested_value( + self._users, str(id), _Key.SCOPES, default=[]) return scope in scopes + @safe_write def set_user_active_in_channel(self, id: str, channel_id: str, active: bool = True): """Set a user as active (or not) on a given channel""" - self._set_user_channel_prop(id, channel_id, _Key.ACTIVE, active) + util.set_nested_value( + self._users, str(id), _Key.CHANNELS, str(channel_id), _Key.ACTIVE, value=active) + util.set_nested_value( + self._users, str(id), _Key.CHANNELS, str(channel_id), _Key.REACTIVATE, value=None) def get_user_active_in_channel(self, id: str, channel_id: str) -> bool: - """Get a users active channels""" - user = self._users.get(str(id), {}) - channels = user.get(_Key.CHANNELS, {}) - return str(channel_id) in [channel for (channel, props) in channels.items() if props.get(_Key.ACTIVE, False)] + """Get a if a user is active in a channel""" + return util.get_nested_value(self._users, str(id), _Key.CHANNELS, str(channel_id), _Key.ACTIVE) + def get_user_paused_in_channel(self, id: str, channel_id: str) -> str: + """Get a the user reactivate time if it exists""" + return util.get_nested_value(self._users, str(id), _Key.CHANNELS, str(channel_id), _Key.REACTIVATE) + + @safe_write def set_user_paused_in_channel(self, id: str, channel_id: str, until: datetime): - """Sets a user as paused in a channel""" - # Deactivate the user in the channel first - self.set_user_active_in_channel(id, channel_id, False) - - self._set_user_channel_prop( - id, channel_id, _Key.REACTIVATE, datetime_to_ts(until)) + """Sets a user as inactive in a channel with a reactivation time""" + util.set_nested_value( + self._users, str(id), _Key.CHANNELS, str(channel_id), _Key.ACTIVE, value=False) + util.set_nested_value( + self._users, str(id), _Key.CHANNELS, str(channel_id), _Key.REACTIVATE, value=datetime_to_ts(until)) @safe_write def reactivate_users(self, channel_id: str): """Reactivate any users who've passed their reactivation time on this channel""" - for user in self._users.values(): - channels = user.get(_Key.CHANNELS, {}) - channel = channels.get(str(channel_id), {}) - if channel and not channel[_Key.ACTIVE]: - reactivate = channel.get(_Key.REACTIVATE, None) - # Check if we've gone past the reactivation time and re-activate - if reactivate and datetime.now() > ts_to_datetime(reactivate): - channel[_Key.ACTIVE] = True - del channel[_Key.REACTIVATE] + for user in self._users: + reactivate = self.get_user_paused_in_channel( + str(user), str(channel_id)) + if reactivate and datetime.now() > ts_to_datetime(reactivate): + self.set_user_active_in_channel(str(user), str(channel_id)) def get_active_match_tasks(self, time: datetime | None = None) -> Generator[str, int]: """ @@ -376,14 +379,6 @@ class State(): def _tasks(self) -> dict[str]: return self._dict[_Key.TASKS] - @safe_write - def _set_user_channel_prop(self, id: str, channel_id: str, key: str, value): - """Set a user channel property helper""" - user = self._users.setdefault(str(id), {}) - channels = user.setdefault(_Key.CHANNELS, {}) - channel = channels.setdefault(str(channel_id), {}) - channel[key] = value - def load_from_file(file: str) -> State: """ diff --git a/matchy/util.py b/matchy/util.py index d284c0d..1f484dc 100644 --- a/matchy/util.py +++ b/matchy/util.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta +from functools import reduce def get_day_with_suffix(day): @@ -42,3 +43,19 @@ def iterate_all_shifts(list: list): for _ in range(len(list)-1): list = list[1:] + [list[0]] yield list + + +def get_nested_value(d, *keys, default=None): + """Helper method for walking down an optional set of nested dicts to get a value""" + return reduce(lambda d, key: d.get(key, {}), keys, d) or default + + +def set_nested_value(d, *keys, value=None): + """Helper method for walking down an optional set of nested dicts to set a value""" + for key in keys[:-1]: + d = d.setdefault(key, {}) + leaf = keys[-1] + if value is not None: + d[leaf] = value + elif leaf in d: + del d[leaf] diff --git a/tests/util_test.py b/tests/util_test.py index 0e8a599..7b9db78 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -10,3 +10,31 @@ def test_iterate_all_shifts(): [3, 4, 1, 2], [4, 1, 2, 3], ] + + +def test_get_nested_dict_value(): + d = { + "x": { + "y": { + "z": { + "val": 42 + } + } + } + } + assert 42 == util.get_nested_value(d, "x", "y", "z", "val") + assert 16 == util.get_nested_value(d, "x", "y", "z", "vol", default=16) + + +def test_set_nested_dict_value(): + d = { + "x": { + "y": { + "z": { + "val": 42 + } + } + } + } + util.set_nested_value(d, "x", "y", "z", "val", value=52) + assert 52 == util.get_nested_value(d, "x", "y", "z", "val")