Fix /leave not working for anyone who's paused in the past
Some checks failed
Test, Build and Publish / test (push) Has been cancelled
Test, Build and Publish / build-and-push-images (push) Has been cancelled

We now clear the re-activate value when a user is unpaused.

Added bonus here is various bits of refactor and cleanup, with some tests
This commit is contained in:
Marc Di Luzio 2024-08-17 14:14:45 +01:00
parent 946de66f52
commit f926a36069
5 changed files with 91 additions and 37 deletions

View file

@ -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)

View file

@ -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):

View file

@ -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:
"""

View file

@ -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]

View file

@ -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")