From 1ddfa7403be16effadcb3510bd0a2f1ebcaea147 Mon Sep 17 00:00:00 2001 From: Jacob Henry Date: Mon, 12 Nov 2018 13:27:46 -0500 Subject: [PATCH] Added reply callback procedure? --- client_wrapper.py | 1 + house_management.py | 4 +++- job_commands.py | 46 +++++++++++++++++++++++++++++++++------------ scroll_util.py | 7 ++++--- slack_util.py | 39 +++++++++++++++++++++++++++++++++++++- 5 files changed, 80 insertions(+), 17 deletions(-) diff --git a/client_wrapper.py b/client_wrapper.py index 8abf957..f8977fb 100644 --- a/client_wrapper.py +++ b/client_wrapper.py @@ -70,6 +70,7 @@ class ClientWrapper(object): # Strip garbage msg['text'] = msg['text'].strip() print("Recv: \"{}\"".format(msg['text'])) + print(msg) # Msg is good # Find which hook, if any, satisfies diff --git a/house_management.py b/house_management.py index c99fe9d..58d18ee 100644 --- a/house_management.py +++ b/house_management.py @@ -36,6 +36,9 @@ class Job(object): # Extra stuff, interpreted day: Optional[date] + def pretty_fmt(self) -> str: + return "{} - {} at {}".format(self.name, self.day_of_week, self.house) + @dataclass class JobAssignment(object): @@ -53,7 +56,6 @@ class JobAssignment(object): late = "y" if self.late else "n" return self.job.name, self.job.house, self.job.day_of_week, self.assignee.name, signer_name, late - @dataclass class PointStatus(object): """ diff --git a/job_commands.py b/job_commands.py index 043478c..ce7ea8f 100644 --- a/job_commands.py +++ b/job_commands.py @@ -63,11 +63,9 @@ def do_signoff(slack: SlackClient, msg: dict, on_assign_index: int, by_brother: house_management.export_points(headers, points) # Then we respond cool! - slack_util.reply(slack, msg, "{} signed off {} for {} {} {}".format(on_assign.signer.name, - on_assign.assignee.name, - on_assign.job.house, - on_assign.job.day_of_week, - on_assign.job.name)) + slack_util.reply(slack, msg, "{} signed off {} for {}".format(on_assign.signer.name, + on_assign.assignee.name, + on_assign.job.pretty_fmt())) alert_user(slack, on_assign.assignee, "Your house job was signed off") @@ -108,7 +106,6 @@ async def signoff_callback(slack: SlackClient, msg: dict, match: Match) -> None: if len(closest_assigns) == 0: slack_util.reply(slack, msg, "Unable to find any jobs assigned to brother {} " "(identified as {}).".format(signee_name, signee.name)) - return # If theres only one job, sign it off elif len(closest_assigns) == 1: @@ -118,12 +115,37 @@ async def signoff_callback(slack: SlackClient, msg: dict, match: Match) -> None: targ_assign_index = assigns.index(targ_assign) do_signoff(slack, msg, targ_assign_index, signer) - return # If theres multiple jobs, we need to get a follow up! else: - slack_util.reply(slack, msg, "Dunno how to handle multiple jobs yet") - return + # Say we need more info + job_list = "\n".join("{}: {}".format(i, a.job.pretty_fmt()) for i, a in enumerate(closest_assigns)) + slack_util.reply(slack, msg, "Multiple sign off options dectected for brother {}.\n" + "Please enter the number corresponding to the job you wish to " + "sign off:\n{}\nIf you do not respond within 60 seconds, the signoff will " + "expire.".format(signee_name, job_list)) + + # Establish a follow up command pattern + pattern = r"\d+" + + # Make the follow up callback + async def foc(_slack: SlackClient, _msg: dict, _match: Match) -> None: + # Get the number out + index = int(_match.group(0)) + + # Check that its valid + if 0 <= index < len(closest_assigns): + # We now know what we're trying to sign off! + specific_targ_assign = closest_assigns[index] + specific_targ_assign_index = assigns.index(specific_targ_assign) + do_signoff(_slack, _msg, specific_targ_assign_index, signer) + else: + # They gave a bad index, or we were unable to find the assignment again. + slack_util.reply(_slack, _msg, "Invalid job index / job unable to be found. Start over from the " + "signoff step.") + + # Make a listener hook + slack_util.ReplyWaiter(foc, pattern, msg["ts"], 60) # noinspection PyUnusedLocal @@ -191,12 +213,12 @@ async def nag_callback(slack, msg, match): signoff_hook = slack_util.Hook(signoff_callback, - pattern=r"testsignoff\s+(.*)", + pattern=r"signoff\s+(.*)", channel_whitelist=[channel_util.HOUSEJOBS]) reset_hook = slack_util.Hook(reset_callback, - pattern=r"testreset signoffs", - channel_whitelist=[channel_util.COMMAND_CENTER_ID]) # COMMAND_CENTER_ID + pattern=r"reset signoffs", + channel_whitelist=[channel_util.COMMAND_CENTER_ID]) nag_hook = slack_util.Hook(nag_callback, pattern=r"nagjobs\s*(.*)", diff --git a/scroll_util.py b/scroll_util.py index 6994ab9..6961d70 100644 --- a/scroll_util.py +++ b/scroll_util.py @@ -35,6 +35,7 @@ brothers_matches = [m for m in brothers_matches if m] brothers: List[Brother] = [Brother(m.group(2), int(m.group(1))) for m in brothers_matches] recent_brothers: List[Brother] = brothers[700:] + async def scroll_callback(slack: SlackClient, msg: dict, match: Match) -> None: """ Finds the scroll of a brother, or the brother of a scroll, based on msg text. @@ -54,7 +55,7 @@ async def scroll_callback(slack: SlackClient, msg: dict, match: Match) -> None: result = "Couldn't find brother {}".format(query) # Respond - slack_util.reply(slack, msg, result) + print(slack_util.reply(slack, msg, result)) def find_by_scroll(scroll: int) -> Optional[Brother]: @@ -82,7 +83,7 @@ class BadName(Exception): "match ratio {}. Please type name better.".format(self.name, self.score, self.threshold) -def find_by_name(name: str, threshold: Optional[float] = None, recent_only: bool=False) -> Brother: +def find_by_name(name: str, threshold: Optional[float] = None, recent_only: bool = False) -> Brother: """ Looks up a brother by name. Raises exception if threshold provided and not met. @@ -110,4 +111,4 @@ def find_by_name(name: str, threshold: Optional[float] = None, recent_only: bool raise BadName(found, score, threshold) -scroll_hook = slack_util.Hook(scroll_callback, pattern=r"scroll\s+(.*)") +scroll_hook = slack_util.Hook(scroll_callback, pattern=r"testscroll\s+(.*)") diff --git a/slack_util.py b/slack_util.py index 38f2d4a..585c2b5 100644 --- a/slack_util.py +++ b/slack_util.py @@ -1,5 +1,5 @@ import re -from time import sleep +from time import sleep, time from typing import Any, Optional, Generator, Match, Callable, List, Coroutine from slackclient import SlackClient @@ -141,10 +141,47 @@ class Hook(AbsHook): return self.callback(slack, msg, match) +class ReplyWaiter(AbsHook): + """ + A special hook that only cares about replies to a given message. + """ + + def __init__(self, callback: Callback, pattern: str, thread_ts: str, lifetime: float): + super().__init__(True) + self.callback = callback + self.pattern = pattern + self.thread_ts = thread_ts + self.lifetime = lifetime + self.start_time = time() + self.dead = False + + def try_apply(self, slack: SlackClient, msg: dict) -> Optional[Coroutine[None, None, None]]: + # First check: are we dead of age yet? + time_alive = time() - self.start_time + should_expire = time_alive < self.lifetime + + # If so, give up the ghost + if self.dead or should_expire: + raise DeadHook() + + # Otherwise proceed normally + # Is the msg the one we care about? If not, ignore + if msg.get("thread_ts", None) != self.thread_ts: + return None + + # Does it match the regex? if not, ignore + match = re.match(self.pattern, msg['text'], flags=re.IGNORECASE) + if match: + return self.callback(slack, msg, match) + else: + return None + + class Passive(object): """ Base class for Periodical tasks, such as reminders and stuff """ + async def run(self, slack: SlackClient) -> None: # Run this passive routed through the specified slack client. raise NotImplementedError()