diff --git a/client_wrapper.py b/client_wrapper.py index f8977fb..6d14896 100644 --- a/client_wrapper.py +++ b/client_wrapper.py @@ -5,14 +5,13 @@ from slackclient import SlackClient # Obvious import channel_util import slack_util -from dummy import FakeClient # Read the API token api_file = open("apitoken.txt", 'r') SLACK_API = next(api_file).strip() api_file.close() -# Enable to use dummy +# Enable to do single-threaded and have better exceptions DEBUG_MODE = False @@ -25,10 +24,7 @@ class ClientWrapper(object): def __init__(self): # Init slack - if DEBUG_MODE: - self.slack = FakeClient() - else: - self.slack = SlackClient(SLACK_API) + self.slack = SlackClient(SLACK_API) # Hooks go regex -> callback on (slack, msg, match) self.hooks: List[slack_util.AbsHook] = [] @@ -53,7 +49,9 @@ class ClientWrapper(object): """ Asynchronous tasks that eternally reads and responds to messages. """ - async for _ in self.spool_tasks(): + async for t in self.spool_tasks(): + if DEBUG_MODE: + await t print("Handling a message...!") async def spool_tasks(self) -> AsyncGenerator[asyncio.Task, Any]: diff --git a/dummy.py b/dummy.py deleted file mode 100644 index 15450c6..0000000 --- a/dummy.py +++ /dev/null @@ -1,29 +0,0 @@ -import time -import channel_util - -roll_msg_1 = { - "type": "message", - "channel": channel_util.SLAVES_TO_THE_MACHINE_ID, - "user": "U0Q1PKL92", - "text": "flaked 3 rolled 2 washed 1", - "ts": "1355517523.000005" -} - -dump_roll_msg = { - "type": "message", - "channel": channel_util.COMMAND_CENTER_ID, - "user": "U0Q1PKL92", - "text": "dump towel data", - "ts": "1355517523.000005" -} - -class FakeClient(object): - def rtm_send_message(self, channel=None, message="", thread=None, to_channel=None): - print("Sent \"{}\" to channel {}".format(message, channel)) - - def rtm_connect(self, with_team_state=None, auto_reconnect=None): - return True - - def rtm_read(self): - time.sleep(4) - return [dump_roll_msg] diff --git a/house_management.py b/house_management.py index 26fc605..7805676 100644 --- a/house_management.py +++ b/house_management.py @@ -88,7 +88,7 @@ def strip_all(l: List[str]) -> List[str]: return [x.strip() for x in l] -def import_assignments() -> List[Optional[JobAssignment]]: +async def import_assignments() -> List[Optional[JobAssignment]]: """ Imports Jobs and JobAssignments from the sheet. 1:1 row correspondence. """ @@ -152,8 +152,8 @@ def import_assignments() -> List[Optional[JobAssignment]]: # Now make an assignment for the job # Find the brother it is assigned to try: - assignee = scroll_util.find_by_name(assignee, SHEET_LOOKUP_THRESHOLD) - except scroll_util.BadName: + assignee = await scroll_util.find_by_name(assignee, SHEET_LOOKUP_THRESHOLD) + except scroll_util.BrotherNotFound: # If we can't get one close enough, make a dummy assignee = scroll_util.Brother(assignee, scroll_util.MISSINGBRO_SCROLL) @@ -163,7 +163,7 @@ def import_assignments() -> List[Optional[JobAssignment]]: signer = None else: signer = scroll_util.find_by_name(signer) - except scroll_util.BadName: + except scroll_util.BrotherNotFound: # If we can't figure out the name signer = None @@ -183,7 +183,7 @@ def import_assignments() -> List[Optional[JobAssignment]]: return assignments -def export_assignments(assigns: List[Optional[JobAssignment]]) -> None: +async def export_assignments(assigns: List[Optional[JobAssignment]]) -> None: # Smash to rows rows = [] for v in assigns: @@ -196,7 +196,7 @@ def export_assignments(assigns: List[Optional[JobAssignment]]) -> None: google_api.set_sheet_range(SHEET_ID, job_range, rows) -def import_points() -> (List[str], List[PointStatus]): +async def import_points() -> (List[str], List[PointStatus]): # Figure out how many things there are in a point status field_count = len(dataclasses.fields(PointStatus)) @@ -208,7 +208,7 @@ def import_points() -> (List[str], List[PointStatus]): point_rows = point_rows[1:] # Tidy rows up - def converter(row: List[Any]) -> Optional[PointStatus]: + async def converter(row: List[Any]) -> Optional[PointStatus]: # If its too long, or empty already, ignore if len(row) == 0 or len(row) > field_count: return None @@ -225,14 +225,14 @@ def import_points() -> (List[str], List[PointStatus]): row[i] = x # Get the brother for the last item - real_brother = scroll_util.find_by_name(row[0], recent_only=True) + real_brother = await scroll_util.find_by_name(row[0]) # Ok! Now, we just map it directly to a PointStatus status = PointStatus(row[0], real_brother, *(row[1:])) return status # Perform conversion and return - point_statuses = [converter(row) for row in point_rows] + point_statuses = [await converter(row) for row in point_rows] return headers, point_statuses diff --git a/identifier.py b/identifier.py index 9a8de64..9d5d779 100644 --- a/identifier.py +++ b/identifier.py @@ -3,12 +3,12 @@ Allows users to register their user account as a specific scroll """ import shelve -from typing import Optional, List, Match +from typing import List, Match from slackclient import SlackClient -import slack_util import scroll_util +import slack_util # The following db maps SLACK_USER_ID -> SCROLL_INTEGER DB_NAME = "user_scrolls" @@ -97,18 +97,20 @@ async def name_callback(slack, msg, match): slack_util.reply(slack, msg, result) -def lookup_msg_brother(msg: dict) -> Optional[scroll_util.Brother]: +async def lookup_msg_brother(msg: dict) -> scroll_util.Brother: """ Finds the real-world name of whoever posted msg. Utilizes their bound-scroll. + :raises BrotherNotFound: :return: brother dict or None """ - return lookup_slackid_brother(msg.get("user")) + return await lookup_slackid_brother(msg.get("user")) -def lookup_slackid_brother(slack_id: str) -> Optional[scroll_util.Brother]: +async def lookup_slackid_brother(slack_id: str) -> scroll_util.Brother: """ Gets whatever brother the userid is registered to + :raises BrotherNotFound: :return: Brother object or None """ with shelve.open(DB_NAME) as db: @@ -116,7 +118,7 @@ def lookup_slackid_brother(slack_id: str) -> Optional[scroll_util.Brother]: scroll = db[slack_id] return scroll_util.find_by_scroll(scroll) except ValueError: - return None + raise scroll_util.BrotherNotFound("Slack id {} not tied to brother".format(slack_id)) def lookup_brother_userids(brother: scroll_util.Brother) -> List[str]: diff --git a/job_commands.py b/job_commands.py index be34f7c..632fc66 100644 --- a/job_commands.py +++ b/job_commands.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from typing import List, Match, Callable, TypeVar, Optional, Iterable from fuzzywuzzy import fuzz @@ -32,100 +33,100 @@ def alert_user(slack: SlackClient, brother: scroll_util.Brother, saywhat: str) - T = TypeVar("T") -def tiemax(items: Iterable[T], key: Callable[[T], float], min_score: Optional[float] = None) -> List[T]: +def tiemax(items: Iterable[T], key: Callable[[T], Optional[float]]) -> List[T]: best = [] - best_score = min_score + best_score = None for elt in items: + # Compute the score score = key(elt) - if best_score is None or score > best_score: + + # Ignore blank scores + if score is None: + continue + # Check if its the new best, wiping old if so + elif best_score is None or score > best_score: best_score = score best = [elt] + # Check if its same as last best elif score == best_score: best.append(elt) return best -def do_signoff(slack: SlackClient, msg: dict, on_assign_index: int, by_brother: scroll_util.Brother) -> None: - # First things first: Get the signoffs - assignments = house_management.import_assignments() - - # Get the one we want - on_assign = assignments[on_assign_index] - - # Modify it - on_assign.signer = by_brother - - # Put them all back - house_management.export_assignments(assignments) - - # Then we update points for house jobs - headers, points = house_management.import_points() - house_management.apply_house_points(points, assignments) - 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.pretty_fmt())) - alert_user(slack, on_assign.assignee, "{} signed you off for {}.".format(on_assign.signer.name, - on_assign.job.pretty_fmt())) +@dataclass +class _ModJobContext: + signer: scroll_util.Brother # The brother invoking the command + assign: house_management.JobAssignment # The job assignment to modify -async def signoff_callback(slack: SlackClient, msg: dict, match: Match) -> None: +async def _mod_jobs(slack: SlackClient, + msg: dict, + relevance_scorer: Callable[[house_management.JobAssignment], Optional[float]], + modifier: Callable[[_ModJobContext], None], + no_job_msg: str = None + ) -> None: """ - Callback to signoff a user. + Stub function that handles various tasks relating to modifying jobs + :param relevance_scorer: Function scores job assignments on relevance. Determines which gets modified + :param modifier: Callback function to modify a job. Only called on a successful operation, and only on one job """ - # Find out who this is - signee_name = match.group(1) + # Make an error wrapper + verb = slack_util.VerboseWrapper(slack, msg) - # Fix with a quick lookup - try: - signee = scroll_util.find_by_name(signee_name, MIN_RATIO, recent_only=True) - except scroll_util.BadName as e: - slack_util.reply(slack, msg, e.as_response()) - return - - # Also, who just signed us off? - signer = identifier.lookup_msg_brother(msg) + # Who invoked this command? + signer = await verb(identifier.lookup_msg_brother(msg)) # Get all of the assignments - assigns = house_management.import_assignments() + assigns = await verb(house_management.import_assignments()) - # Find closest assignment to what we're after - def scorer(a: Optional[house_management.JobAssignment]) -> float: + # Find closest assignment to what we're after. This just wraps relevance_scorer to handle nones. + def none_scorer(a: Optional[house_management.JobAssignment]) -> Optional[float]: if a is None: - return 0 + return None else: - return fuzz.ratio(signee.name, a.assignee.name) + return relevance_scorer(a) - closest_assigns = tiemax(assigns, key=scorer) + closest_assigns = tiemax(assigns, key=none_scorer) - # Remove those that are already signed off - closest_assigns = [c for c in closest_assigns if c is not None] - closest_assigns = [c for c in closest_assigns if c.signer is None] + # This is what we do on success. It will or won't be called immediately based on what's in closest_assigns + async def success_callback(targ_assign: house_management.JobAssignment) -> None: + # First get the most up to date version of the jobs + fresh_assigns = await verb(house_management.import_assignments()) + + # Find the one that matches what we had before + fresh_targ_assign = fresh_assigns[fresh_assigns.index(targ_assign)] + + # Create the context + context = _ModJobContext(signer, fresh_targ_assign) + + # Modify it + modifier(context) + + # Re-upload + await house_management.export_assignments(fresh_assigns) + + # Also import and update points + headers, points = await house_management.import_points() + house_management.apply_house_points(points, fresh_assigns) + house_management.export_points(headers, points) # If there aren't any jobs, say so 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)) + if no_job_msg is None: + no_job_msg = "Unable to find any jobs to apply this command to. Try again with better spelling or whatever." + slack_util.reply(slack, msg, no_job_msg) # If theres only one job, sign it off elif len(closest_assigns) == 1: - targ_assign = closest_assigns[0] - - # Where is it? - targ_assign_index = assigns.index(targ_assign) - - do_signoff(slack, msg, targ_assign_index, signer) + await success_callback(closest_assigns[0]) # If theres multiple jobs, we need to get a follow up! else: # 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)) + slack_util.reply(slack, msg, "Multiple relevant job listings found.\n" + "Please enter the number corresponding to the job " + "you wish to modify:\n{}".format(job_list)) # Establish a follow up command pattern pattern = r"\d+" @@ -138,19 +139,68 @@ async def signoff_callback(slack: SlackClient, msg: dict, match: Match) -> None: # 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) + await success_callback(closest_assigns[index]) 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.") + slack_util.reply(_slack, _msg, "Invalid job index / job unable to be found.") # Make a listener hook - new_hook = slack_util.ReplyWaiter(foc, pattern, msg["ts"], 60) + new_hook = slack_util.ReplyWaiter(foc, pattern, msg["ts"], 120) + + # Register it client_wrapper.get_client_wrapper().add_hook(new_hook) +async def signoff_callback(slack: SlackClient, msg: dict, match: Match) -> None: + verb = slack_util.VerboseWrapper(slack, msg) + + # Find out who we are trying to sign off is + signee_name = match.group(1) + signee = await verb(scroll_util.find_by_name(signee_name, MIN_RATIO)) + + # Score by name similarity, only accepting non-assigned jobs + def scorer(assign: house_management.JobAssignment): + if assign.signer is None: + return fuzz.ratio(signee.name, assign.assignee.name) + + # Set the assigner, and notify + def modifier(context: _ModJobContext): + context.assign.signer = context.signer + + # Say we did it wooo! + slack_util.reply(slack, msg, "Signed off {} for {}".format(context.assign.assignee.name, + context.assign.job.name)) + alert_user(slack, context.assign.assignee, "{} signed you off for {}.".format(context.assign.signer.name, + context.assign.job.pretty_fmt())) + + # Fire it off + await _mod_jobs(slack, msg, scorer, modifier) + + +async def late_callback(slack: SlackClient, msg: dict, match: Match) -> None: + verb = slack_util.VerboseWrapper(slack, msg) + + # Find out who we are trying to sign off is + signee_name = match.group(1) + signee = await verb(scroll_util.find_by_name(signee_name, MIN_RATIO)) + + # Score by name similarity. Don't care if signed off or not + def scorer(assign: house_management.JobAssignment): + return fuzz.ratio(signee.name, assign.assignee.name) + + # Just set the assigner + def modifier(context: _ModJobContext): + context.assign.late = not context.assign.late + + # Say we did it + slack_util.reply(slack, msg, "Toggled lateness of {}.\n" + "Now marked as late: {}".format(context.assign.job.pretty_fmt(), + context.assign.late)) + + # Fire it off + await _mod_jobs(slack, msg, scorer, modifier) + + # noinspection PyUnusedLocal async def reset_callback(slack: SlackClient, msg: dict, match: Match) -> None: """ @@ -167,11 +217,11 @@ async def reset_callback(slack: SlackClient, msg: dict, match: Match) -> None: house_management.export_points(headers, points) # Now unsign everything - assigns = house_management.import_assignments() + assigns = await house_management.import_assignments() for a in assigns: if a is not None: a.signer = None - house_management.export_assignments(assigns) + await house_management.export_assignments(assigns) slack_util.reply(slack, msg, "Reset scores and signoffs") @@ -181,7 +231,7 @@ async def nag_callback(slack, msg, match): day = match.group(1).lower().strip() # Get the assigns - assigns = house_management.import_assignments() + assigns = await house_management.import_assignments() # Filter to day assigns = [assign for assign in assigns if assign is not None and assign.job.day_of_week.lower() == day] @@ -219,6 +269,10 @@ signoff_hook = slack_util.Hook(signoff_callback, pattern=r"signoff\s+(.*)", channel_whitelist=[channel_util.HOUSEJOBS]) +late_hook = slack_util.Hook(late_callback, + pattern=r"marklate\s+(.*)", + channel_whitelist=[channel_util.HOUSEJOBS]) + reset_hook = slack_util.Hook(reset_callback, pattern=r"reset signoffs", channel_whitelist=[channel_util.COMMAND_CENTER_ID]) diff --git a/main.py b/main.py index b9961cf..7225bb1 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,8 @@ import asyncio +import textwrap +from typing import Match + +from slackclient import SlackClient import channel_util import client_wrapper @@ -6,6 +10,7 @@ import identifier import job_commands import management_commands import scroll_util +import slack_util import slavestothemachine @@ -24,9 +29,6 @@ def main() -> None: # Added channel utility wrap.add_hook(channel_util.channel_check_hook) - # Add nagging functionality - wrap.add_hook(job_commands.nag_hook) - # Add kill switch wrap.add_hook(management_commands.reboot_hook) @@ -34,13 +36,14 @@ def main() -> None: wrap.add_hook(slavestothemachine.count_work_hook) wrap.add_hook(slavestothemachine.dump_work_hook) - # Add signoffs + # Add job management wrap.add_hook(job_commands.signoff_hook) + wrap.add_hook(job_commands.late_hook) wrap.add_hook(job_commands.reset_hook) + wrap.add_hook(job_commands.nag_hook) # Add help - # help_callback = management_commands.list_hooks_callback_gen(wrap.hooks) - # wrap.add_hook(slack_util.Hook(help_callback, pattern=management_commands.bot_help_pattern)) + wrap.add_hook(slack_util.Hook(help_callback, pattern=management_commands.bot_help_pattern)) # Add boozebot # wrap.add_passive(periodicals.ItsTenPM()) @@ -53,6 +56,33 @@ def main() -> None: event_loop.run_until_complete(both) +async def help_callback(slack: SlackClient, msg: dict, match: Match) -> None: + slack_util.reply(slack, msg, textwrap.dedent(""" + Commands are as follows. Note that some only work in certain channels. + "my scroll is " : Registers your slack account to have a certain scroll, for the purpose of automatic dm's. + "@person has scroll " : same as above, but for other users. Helpful if they are being obstinate. + "what is my scroll" : Echos back what the bot thinks your scroll is. Largely for debugging. + "what is my name" : Echos back what the bot thinks your name is. Largely for debugging. If you want to change this, you'll need to fix the "Sorted family tree" file that the bot reads. Sorry. + "channel id #wherever" : Debug command to get a slack channels full ID + "reboot" : Restarts the server. + "dump towel data" : Summarize the towel roller work for the week, and reset it. + "signoff " : Sign off a brother's house job. Will prompt for more information if needed. + "marklate " : Same as above, but to mark a job as being completed but having been done late. + IN PROGRESS: "reassign -> " : Reassign a house job. + "nagjobs " : Notify in general the house jobs for the week. + "reset signoffs" : Clear points for the week, and undo all signoffs. Not frequently useful. + "help" : You stupid or something? You're reading it. This is all it does. What do you want from me? + + --- + + Also of note is that in #slavestothemachine, any wording of the format "replaced ", or similarly with "washed", "dried", "rolled", or "flaked", will track your effort for the week. + + Github is https://github.com/TheVillageIdiot2/waitonbot + Man in charge is Jacob Henry, but nothing lasts forever. + """)) + # Do not let my efforts fall to waste. Its a pitious legacy but its something, at least, to maybe tide the + # unending flow of work for poor Niko. + # run main if __name__ == '__main__': main() diff --git a/management_commands.py b/management_commands.py index 778848b..d4564e2 100644 --- a/management_commands.py +++ b/management_commands.py @@ -23,7 +23,7 @@ async def reboot_callback(slack: SlackClient, msg: dict, match: Match) -> None: # Make hooks -bot_help_pattern = r"bot help" # Can't init this directly, as it relies on us knowing all other hooks. handle in main +bot_help_pattern = r"help" # Can't init this directly, as it relies on us knowing all other hooks. handle in main reboot_hook = slack_util.Hook(reboot_callback, pattern=r"reboot", channel_whitelist=[channel_util.COMMAND_CENTER_ID]) diff --git a/scroll_util.py b/scroll_util.py index cd64b75..1fbaadc 100644 --- a/scroll_util.py +++ b/scroll_util.py @@ -33,7 +33,6 @@ brother_match = re.compile(r"([0-9]*)~(.*)") brothers_matches = [brother_match.match(line) for line in familyfile] 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: @@ -44,11 +43,15 @@ async def scroll_callback(slack: SlackClient, msg: dict, match: Match) -> None: query = match.group(1).strip() # Try to get as int or by name + result = None try: sn = int(query) result = find_by_scroll(sn) except ValueError: - result = find_by_name(query) + try: + result = await find_by_name(query) + except BrotherNotFound: + pass if result: result = "Brother {} has scroll {}".format(result.name, result.scroll) else: @@ -72,43 +75,36 @@ def find_by_scroll(scroll: int) -> Optional[Brother]: # Used to track a sufficiently shitty typed name -class BadName(Exception): - def __init__(self, name: str, score: float, threshold: float): - self.name = name - self.score = score - self.threshold = threshold - - def as_response(self) -> str: - return "Unable to perform operation. Best name match {} had a match score of {}, falling short of minimum " \ - "match ratio {}. Please type name better.".format(self.name, self.score, self.threshold) +class BrotherNotFound(Exception): + """Throw when we can't find the desired brother.""" + pass -def find_by_name(name: str, threshold: Optional[float] = None, recent_only: bool = False) -> Brother: +async def find_by_name(name: str, threshold: Optional[float] = None) -> Brother: """ Looks up a brother by name. Raises exception if threshold provided and not met. :param threshold: Minimum match ratio to accept. Can be none. :param name: The name to look up, with a fuzzy search - :param recent_only: Whether or not to only search more recent undergrad brothers. + :raises BrotherNotFound: :return: The best-match brother - """ - # Pick a list - if recent_only: - bros_to_use = recent_brothers - else: - bros_to_use = brothers - - # Get all of the names - all_names = [b.name for b in bros_to_use] + """ # Get all of the names + all_names = [b.name for b in brothers] # Do fuzzy match found, score = process.extractOne(name, all_names) score = score / 100.0 + found_index = all_names.index(found) + found_brother = brothers[found_index] if (not threshold) or score > threshold: - found_index = all_names.index(found) - return bros_to_use[found_index] + return found_brother else: - raise BadName(found, score, threshold) + msg = "Couldn't find brother {}. Best match \"{}\" matched with accuracy {}, falling short of safety minimum " \ + "accuracy {}. Please type name more accurately, to prevent misfires.".format(name, + found_brother, + score, + threshold) + raise BrotherNotFound(msg) scroll_hook = slack_util.Hook(scroll_callback, pattern=r"scroll\s+(.*)") diff --git a/slack_util.py b/slack_util.py index c353790..9f68e98 100644 --- a/slack_util.py +++ b/slack_util.py @@ -1,4 +1,5 @@ import re +import typing from time import sleep, time from typing import Any, Optional, Generator, Match, Callable, List, Coroutine @@ -77,6 +78,26 @@ def message_stream(slack: SlackClient) -> Generator[dict, None, None]: print("Connection failed - retrying") +T = typing.TypeVar("T") + + +class VerboseWrapper(Callable): + """ + Generates exception-ready delegates. + Warns of exceptions as they are passed through it, via responding to the given message. + """ + def __init__(self, slack: SlackClient, command_msg: dict): + self.slack = slack + self.command_msg = command_msg + + async def __call__(self, awt: typing.Awaitable[T]) -> T: + try: + return await awt + except Exception as e: + reply(self.slack, self.command_msg, "Error: {}".format(str(e)), True) + raise e + + MsgAction = Coroutine[Any, Any, None] Callback = Callable[[SlackClient, dict, Match], MsgAction] diff --git a/slavestothemachine.py b/slavestothemachine.py index b4a9e5e..1dc4ca3 100644 --- a/slavestothemachine.py +++ b/slavestothemachine.py @@ -8,6 +8,8 @@ import identifier import re import shelve +from scroll_util import BrotherNotFound + counted_data = ["flaked", "rolled", "replaced", "washed", "dried"] lookup_format = "{}\s+(\d+)" @@ -25,7 +27,7 @@ async def count_work_callback(slack: SlackClient, msg: dict, match: Match) -> No # Couple things to work through. # One: Who sent the message? - who_wrote = identifier.lookup_msg_brother(msg) + who_wrote = await identifier.lookup_msg_brother(msg) who_wrote_label = "{} [{}]".format(who_wrote.name, who_wrote.scroll) # Two: What work did they do? @@ -75,8 +77,9 @@ async def dump_work_callback(slack: SlackClient, msg: dict, match: Match) -> Non del db[user_id] # Get the name - brother = identifier.lookup_slackid_brother(user_id) - if brother is None: + try: + brother = await identifier.lookup_slackid_brother(user_id) + except BrotherNotFound: brother = user_id else: brother = brother.name