async def __call__(self, awt: Awaitable[T]) -> T: try: return await awt except Exception as e: client.get_slack().reply(self.event, "Error: {}".format(str(e)), True) raise e
async def help_callback(event: slack_util.Event, match: Match) -> None: client.get_slack().reply(event, textwrap.dedent(""" Commands are as follows. Note that some only work in certain channels. "my scroll is number" : Registers your slack account to have a certain scroll, for the purpose of automatic dm's. "@person has scroll number" : 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. "signoff John Doe" : Sign off a brother's house job. Will prompt for more information if needed. "marklate John Doe" : Same as above, but to mark a job as being completed but having been done late. "reassign John Doe -> James Deer" : Reassign a house job. "undo signoff John Doe" : Marks a brother's house job as incomplete. Useful if you f****d up. "nagjobs day" : Notify in general the house jobs for the week. "reset signoffs" : Clear points for the week, and undo all signoffs. Not frequently useful, admin only. "refresh points" : Updates house job / signoff points for the week, after manual edits to the sheet. Admin only. "help" : 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 <number>", or similarly with "washed", "dried", "rolled", or "flaked", will track your effort for the week. Github is https://github.com/whitespine/waitonbot Man in charge is Jacob Henry, but nothing lasts forever. """))
async def clear_callback(event: slack_util.Event, match: Match) -> None: info = [] laundry = LaundryRoom() machine = match.group(1).strip() #Same as before for finding actual machine from input if machine[0].lower() == "w": if machine[1] == "1": laundry.start_machine(4, info) elif machine[1] == "2": laundry.start_machine(5, info) else: raise Exception("No other washers exist") elif machine[0].lower() == "d": if machine[1] == "1": laundry.start_machine(1, info) elif machine[1] == "2": laundry.start_machine(2, info) elif machine[1] == "3": laundry.start_machine(3, info) else: raise Exception("No other dryers exist") else: raise Exception("Only washers and dryers exist") message = "{} cleared".format(machine) client.get_slack().reply(event, message)
async def modifier(context: _ModJobContext): context.assign.late = not context.assign.late # Say we did it client.get_slack().reply( event, "Toggled lateness of {}.\n" "Now marked as late: {}".format(context.assign.job.pretty_fmt(), context.assign.late))
async def nag_callback(event: slack_util.Event, match: Match) -> None: # Get the day day = match.group(1).lower().strip() if not await nag_jobs(day): client.get_slack().reply( event, "No jobs found. Check that the day is spelled correctly, with no extra symbols.\n" "It is possible that all jobs have been signed off, as well.", in_thread=True)
async def modifier(context: _ModJobContext): context.assign.signer = context.signer # Say we did it wooo! client.get_slack().reply( event, "Signed off {} for {}".format(context.assign.assignee.name, context.assign.job.name)) await alert_user( context.assign.assignee, "{} signed you off for {}.".format( context.assign.signer.name, context.assign.job.pretty_fmt()))
def make_interactive_msg(): # Send the message and recover the ts response = client.get_slack().send_message("Select an option:", "#botzone", blocks=[ { "type": "actions", "block_id": "button_test", "elements": [ { "type": "button", "action_id": "alpha_button", "text": { "type": "plain_text", "text": "Alpha", "emoji": False } }, { "type": "button", "action_id": "beta_button", "text": { "type": "plain_text", "text": "Beta", "emoji": False } } ] } ]) msg_ts = response["ts"] botzone = client.get_slack().get_conversation_by_name("#botzone") # Make our mappings button_responses = { "alpha_button": "You clicked alpha. Good work.", "beta_button": "You clicked beta. You must be so proud." } # Make our callbacks async def on_click(event: slack_util.Event, response_str: str): # Edit the message to show the result. client.get_slack().edit_message(response_str, event.conversation.conversation_id, event.message.ts, []) def on_expire(): # Edit the message to show defeat. client.get_slack().edit_message("Timed out", botzone.id, msg_ts, []) # Add a listener listener = hooks.InteractionListener(on_click, button_responses, botzone, msg_ts, lifespan, on_expire) client.get_slack().add_hook(listener)
async def check_callback(event: slack_util.Event, match: Match) -> None: laundry = LaundryRoom() tempResult = "- D1\nD2 W1\nD3 W2\nD1: {} minutes left\nD2: {} minutes left\nD3: {} minutes left\nW1: {} minutes left\nW2: {} minutes left" occupany = await laundry.check_occupany() #Formats it so if its empty it will say empty else it will give the actual info result = tempResult.format(occupany[0] if len(occupany[0]) else "Empty", occupany[1] if len(occupany[1]) else "Empty", occupany[2] if len(occupany[2]) else "Empty", occupany[3] if len(occupany[3]) else "Empty", occupany[4] if len(occupany[4]) else "Empty") client.get_slack().reply(event, result)
async def foc(_event: slack_util.Event, _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! await success_callback(closest_assigns[index]) else: # They gave a bad index, or we were unable to find the assignment again. client.get_slack().reply( _event, "Invalid job index / job unable to be found.")
async def modifier(context: _ModJobContext): context.assign.signer = None # Say we did it wooo! client.get_slack().reply( event, "Undid signoff of {} for {}".format(context.assign.assignee.name, context.assign.job.name)) await alert_user( context.assign.assignee, "{} undid your signoff off for {}.\n" "Must have been a mistake".format(context.assign.signer.name, context.assign.job.pretty_fmt()))
async def modifier(context: _ModJobContext): context.assign.assignee = to_bro # Say we did it reassign_msg = "Job {} reassigned from {} to {}".format( context.assign.job.pretty_fmt(), from_bro, to_bro) client.get_slack().reply(event, reassign_msg) # Tell the people reassign_msg = "Job {} reassigned from {} to {}".format( context.assign.job.pretty_fmt(), from_bro, to_bro) await alert_user(from_bro, reassign_msg) await alert_user(to_bro, reassign_msg)
async def count_work_callback(event: slack_util.Event, match: Match) -> None: # If no user, continue if event.user is None: return # If bot, continue if event.bot is not None: return # Make an error wrapper verb = slack_util.VerboseWrapper(event) # Tidy the text text = event.message.text.strip().lower() # Couple things to work through. # One: Who sent the message? who_wrote = await verb(event.user.as_user().get_brother()) who_wrote_label = "{} [{}]".format(who_wrote.name, who_wrote.scroll) # Two: What work did they do? new_work = {} for job in counted_data: pattern = lookup_format.format(job) match = re.search(pattern, text) if match: new_work[job] = int(match.group(1)) # Three: check if we found anything if len(new_work) == 0: if re.search(r'\s\d\s', text) is not None: client.get_slack().reply( event, "If you were trying to record work, it was not recognized.\n" "Use words {} or work will not be recorded".format( counted_data)) return # Four: Knowing they did something, record to total work contribution_count = sum(new_work.values()) new_total = await verb( record_towel_contribution(who_wrote, contribution_count)) # Five, congratulate them on their work! congrats = textwrap.dedent("""{} recorded work: {} Net increase in points: {} Total points since last reset: {}""".format(who_wrote_label, fmt_work_dict(new_work), contribution_count, new_total)) client.get_slack().reply(event, congrats)
async def alert_user(brother: scroll_util.Brother, saywhat: str) -> None: """ DM a brother saying something. Wrapper around several simpler methods """ # We do this as a for loop just in case multiple people reg. to same scroll for some reason (e.g. dup accounts) succ = False for slack_id in await identifier.lookup_brother_userids(brother): client.get_slack().send_message(saywhat, slack_id) succ = True # Warn if we never find if not succ: logging.warning( "Unable to find dm conversation for brother {}".format(brother))
async def run(self) -> None: while True: # Get 10PM ten_pm = datetime.now().replace(hour=22, minute=0, second=0) # Find out how long until it, then sleep that long delay = seconds_until(ten_pm) await asyncio.sleep(delay) # Crow like a rooster client.get_slack().send_message("IT'S 10 PM!", client .get_slack() .get_conversation_by_name("#random").id) # Wait a while before trying it again, to prevent duplicates await asyncio.sleep(60)
async def run(self) -> None: while True: # Get the end of the current day (Say, 10PM) next_remind_time = datetime.now().replace(hour=22, minute=00, second=0) # If we've accidentally made it in the past somehow, bump it up one date while datetime.now() > next_remind_time: next_remind_time += timedelta(days=1) # Sleep until that time delay = seconds_until(next_remind_time) await asyncio.sleep(delay) # Now it is that time. Get the current jobs assigns = await house_management.import_assignments() # Filter to incomplete, and today assigns: List[house_management.JobAssignment] = [ a for a in assigns if self.is_job_valid(a) ] # Now, we want to nag each person. If we don't actually know who they are, so be it. logging.info( "Scheduled reminding people who haven't yet done their jobs.") for a in assigns: # Get the relevant slack ids assignee_ids = await identifier.lookup_brother_userids( a.assignee) # For each, send them a DM success = False for slack_id in assignee_ids: msg = "{}, you still need to do {}".format( a.assignee.name, a.job.pretty_fmt()) success = True client.get_slack().send_message(msg, slack_id) # Warn on failure if not success: logging.warning( "Tried to nag {} but couldn't find their slack id". format(a.assignee.name)) # Take a break to ensure no double-shots await asyncio.sleep(10)
def main() -> None: wrap = client.get_slack() # Add scroll handling wrap.add_hook(scroll_util.scroll_hook) # Add id handling wrap.add_hook(identifier.check_hook) wrap.add_hook(identifier.identify_hook) wrap.add_hook(identifier.identify_other_hook) wrap.add_hook(identifier.name_hook) # Add kill switch wrap.add_hook(management_commands.reboot_hook) wrap.add_hook(management_commands.log_hook) # Add towel rolling wrap.add_hook(slavestothemachine.count_work_hook) # wrap.add_hook(slavestothemachine.dump_work_hook) # 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) wrap.add_hook(job_commands.reassign_hook) wrap.add_hook(job_commands.refresh_hook) # Add help wrap.add_hook(hooks.ChannelHook(help_callback, patterns=[r"help", r"bot\s+help"])) # Add boozebot # wrap.add_passive(periodicals.ItsTenPM()) #Add laundry wrap.add_hook(laundry.check_hook) wrap.add_hook(laundry.start_hook) wrap.add_hook(laundry.help_hook) wrap.add_hook(laundry.clear_hook) # Add automatic updating of users wrap.add_passive(periodicals.Updatinator(wrap, 120)) # Do test. wrap.add_passive(periodicals.TestPassive()) # Add nagloop wrap.add_passive(periodicals.NotifyJobs()) wrap.add_passive(periodicals.RemindJobs()) event_loop = asyncio.get_event_loop() event_loop.set_debug(settings.USE_ASYNC_DEBUG_MODE) event_handling = wrap.handle_events() passive_handling = wrap.run_passives() both = asyncio.gather(event_handling, passive_handling) event_loop.run_until_complete(both)
async def scroll_callback(event: slack_util.Event, match: Match) -> None: """ Finds the scroll of a brother, or the brother of a scroll, based on msg text. """ # Get the query query = match.group(1).strip() # Try to get as int or by name try: sn = int(query) result = find_by_scroll(sn) except ValueError: result = await find_by_name(query) if result: result = "Brother {} has scroll {}".format(result.name, result.scroll) else: result = "Couldn't find brother {}".format(query) # Respond client.get_slack().reply(event, result)
async def post_log_callback(event: slack_util.Event, match: Match) -> None: # Get the last n lines of log of the specified severity or higher count = 100 lines = [] # numerically rank the debug severity severity_codex = { "CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10, "NOTSET": 0 } curr_rating = 0 # Get the min rating if one exists min_rating = 0 rating_str = match.group(1).upper().strip() for severity_name, severity_value in severity_codex.items(): if severity_name in rating_str: min_rating = severity_value break with open(settings.LOGFILE, 'r') as f: for line in f: # Update the current rating if necessary if line[:3] == "#!#": for k, v in severity_codex.items(): if k in line: curr_rating = v break # Add the line if its severity is at or above the required minimum if curr_rating >= min_rating: lines.append(line) if len(lines) > count: del lines[0] # Spew them out client.get_slack().reply(event, "```" + ''.join(lines) + "```")
async def nag_jobs(day_of_week: str) -> bool: # Get the assigns 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_of_week ] # Filter signed off assigns = [assign for assign in assigns if assign.signer is None] # If no jobs found, somethings up. Probably mispelled day. Return failure if not assigns: return False # Nag each response = "Do yer jerbs! They are as follows:\n" for assign in assigns: # Make the row template if assign.assignee is None: continue response += "({}) {} -- {} ".format(assign.job.house, assign.job.name, assign.assignee.name) # Find the people to @ brother_slack_ids = await identifier.lookup_brother_userids( assign.assignee) if brother_slack_ids: for slack_id in brother_slack_ids: response += "<@{}> ".format(slack_id) else: response += "(scroll missing. Please register for @ pings!)" response += "\n" general_id = client.get_slack().get_conversation_by_name("#general").id client.get_slack().send_message(response, general_id) return True
async def start_callback(event: slack_util.Event, match: Match) -> None: verb = slack_util.VerboseWrapper(event) laundry = LaundryRoom() machine = match.group(1).strip() scroll = match.group(2).strip() timeRemaining = match.group(3).strip() brother = await verb(scroll_util.find_by_scroll(scroll)) info = [brother, int(scroll), datetime.datetime.now(), int(timeRemaining)] #This cluster is for figuring out which machine they actually want to start if machine[0].lower() == "w": if machine[1] == "1": laundry.start_machine(4, info) elif machine[1] == "2": laundry.start_machine(5, info) else: raise Exception("No other washers exist") elif machine[0].lower() == "d": if machine[1] == "1": laundry.start_machine(1, info) elif machine[1] == "2": laundry.start_machine(2, info) elif machine[1] == "3": laundry.start_machine(3, info) else: raise Exception("No other dryers exist") else: raise Exception("Only washers and dryers exist") result = "{} started by {} for {} minutes".format(machine, brother, timeRemaining) client.get_slack().reply(event, result) await asyncio.sleep(timeRemaining * 60) msg = "Your laundry is done in machine {}".format(machine) #Not sure if this way of getting the user will work client.get_slack().send_message(msg, event.user)
async def reset_callback(event: slack_util.Event, match: Match) -> None: """ Resets the scores. """ # Unassign everything assigns = await house_management.import_assignments() for a in assigns: if a is not None: a.signer = None await house_management.export_assignments(assigns) # Now wipe points headers, points = house_management.import_points() # Set to 0/default for i in range(len(points)): new = house_management.PointStatus(brother=points[i].brother) points[i] = new house_management.apply_house_points( points, await house_management.import_assignments()) house_management.export_points(headers, points) client.get_slack().reply(event, "Reset scores and signoffs")
async def _mod_jobs(event: slack_util.Event, relevance_scorer: Callable[ [house_management.JobAssignment], Optional[float]], modifier: Callable[[_ModJobContext], Coroutine[Any, Any, None]], no_job_msg: str = None) -> None: """ 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 """ # Make an error wrapper verb = slack_util.VerboseWrapper(event) # Who invoked this command? signer = await verb(event.user.as_user().get_brother()) # Get all of the assignments assigns = await verb(house_management.import_assignments()) # 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 None else: return relevance_scorer(a) closest_assigns = tiemax(assigns, key=none_scorer) # 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 await 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: 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." client.get_slack().reply(event, no_job_msg) # If theres only one job, sign it off elif len(closest_assigns) == 1: 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)) client.get_slack().reply( event, "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+" # Make the follow up callback async def foc(_event: slack_util.Event, _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! await success_callback(closest_assigns[index]) else: # They gave a bad index, or we were unable to find the assignment again. client.get_slack().reply( _event, "Invalid job index / job unable to be found.") # Make a listener hook new_hook = hooks.ReplyWaiter(foc, pattern, event.message.ts, 120) # Register it client.get_slack().add_hook(new_hook)
async def refresh_callback(event: slack_util.Event, match: Match) -> None: headers, points = await house_management.import_points() house_management.apply_house_points( points, await house_management.import_assignments()) house_management.export_points(headers, points) client.get_slack().reply(event, "Force updated point values")
async def on_click(event: slack_util.Event, response_str: str): # Edit the message to show the result. client.get_slack().edit_message(response_str, event.conversation.conversation_id, event.message.ts, [])
def on_expire(): # Edit the message to show defeat. client.get_slack().edit_message("Timed out", botzone.id, msg_ts, [])
def get_user(self) -> Optional[User]: """ Lookup the user to which this DM corresponds. """ return client.get_slack().get_user(self.user_id)
def get_conversation(self) -> Optional[Conversation]: return client.get_slack().get_conversation(self.conversation_id)
def as_user(self) -> Optional[User]: return client.get_slack().get_user(self.user_id)
async def help_callback(event: slack_util.Event, match: Match) -> None: message = "To start a laundry load type \"start w1 xxxx 50\" where w1 is the washer or dryer with number, xxxx is your scroll, and 50 is the minutes left on the machine\nTo check laundry room occupancy type \"check laundry\"" client.get_slack().reply(event, message)
async def reboot_callback(event: slack_util.Event, match: Match) -> None: response = "Ok. Rebooting..." client.get_slack().reply(event, response) exit(0)