From c1dbe21c77a2352a39c0a0533305ed69a36a4fc2 Mon Sep 17 00:00:00 2001 From: Jacob Henry Date: Fri, 22 Feb 2019 05:27:03 -0500 Subject: [PATCH] Awaiting testing --- job_commands.py | 32 +++++++------- main.py | 3 ++ periodicals.py | 98 ++++++++++++++++++++++++++++++------------- slack_util.py | 85 +++++++++++++++++++++++++++++++++++-- slavestothemachine.py | 3 -- 5 files changed, 170 insertions(+), 51 deletions(-) diff --git a/job_commands.py b/job_commands.py index 4e39256..1be8d7c 100644 --- a/job_commands.py +++ b/job_commands.py @@ -2,7 +2,6 @@ from dataclasses import dataclass from typing import List, Match, Callable, TypeVar, Optional, Iterable from fuzzywuzzy import fuzz -from slackclient import SlackClient import house_management import identifier @@ -124,8 +123,8 @@ async def _mod_jobs(event: slack_util.Event, # Say we need more info job_list = "\n".join("{}: {}".format(i, a.job.pretty_fmt()) for i, a in enumerate(closest_assigns)) slack_util.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)) + "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+" @@ -170,7 +169,7 @@ async def signoff_callback(event: slack_util.Event, match: Match) -> None: # Say we did it wooo! slack_util.get_slack().reply(event, "Signed off {} for {}".format(context.assign.assignee.name, - context.assign.job.name)) + context.assign.job.name)) alert_user(context.assign.assignee, "{} signed you off for {}.".format(context.assign.signer.name, context.assign.job.pretty_fmt())) @@ -198,7 +197,7 @@ async def undo_callback(event: slack_util.Event, match: Match) -> None: # Say we did it wooo! slack_util.get_slack().reply(event, "Undid signoff of {} for {}".format(context.assign.assignee.name, - context.assign.job.name)) + context.assign.job.name)) 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())) @@ -227,8 +226,8 @@ async def late_callback(event: slack_util.Event, match: Match) -> None: # Say we did it slack_util.get_slack().reply(event, "Toggled lateness of {}.\n" - "Now marked as late: {}".format(context.assign.job.pretty_fmt(), - context.assign.late)) + "Now marked as late: {}".format(context.assign.job.pretty_fmt(), + context.assign.late)) # Fire it off await _mod_jobs(event, scorer, modifier) @@ -311,22 +310,27 @@ async def refresh_callback(event: slack_util.Event, match: Match) -> None: 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): + slack_util.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) + +# Wrapper so we can auto-call this as well +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] + 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. + # If no jobs found, somethings up. Probably mispelled day. Return failure if not assigns: - slack_util.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) - return + return False # Nag each response = "Do yer jerbs! They are as follows:\n" @@ -347,7 +351,7 @@ async def nag_callback(event: slack_util.Event, match: Match) -> None: response += "\n" general_id = slack_util.get_slack().get_channel_by_name("#general").id - slack_util.get_slack().reply(event, response, in_thread=False, to_channel=general_id) + slack_util.get_slack().send_message(response, general_id) signoff_hook = slack_util.ChannelHook(signoff_callback, diff --git a/main.py b/main.py index c25b960..feed675 100644 --- a/main.py +++ b/main.py @@ -44,6 +44,9 @@ def main() -> None: # Add boozebot # wrap.add_passive(periodicals.ItsTenPM()) + # Add automatic updating of users + wrap.add_passive(periodicals.Updatinator(wrap, 60)) + # Add nagloop wrap.add_passive(periodicals.RemindJobs()) diff --git a/periodicals.py b/periodicals.py index 35098b5..7cfa04f 100644 --- a/periodicals.py +++ b/periodicals.py @@ -4,6 +4,7 @@ from typing import Optional, List import house_management import identifier +import job_commands import slack_util @@ -30,21 +31,64 @@ class ItsTenPM(slack_util.Passive): await asyncio.sleep(60) -class RemindJobs(slack_util.Passive): +# Shared behaviour +class JobNotifier: + @staticmethod + def get_day_of_week() -> str: + """ + Gets the current day of week as a str + """ + return ["Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday"][datetime.now().weekday()] + + @staticmethod + def is_job_valid(a: Optional[house_management.JobAssignment]): + # If it doesn't exist, it a thot + if a is None: + return False + # If its not today, we shouldn't nag + if a.job.day_of_week.lower() != JobNotifier.get_day_of_week().lower(): + return False + # If it is unassigned, we can't nag + if a.assignee is None: + return False + # If its been signed off, no need to nag + if a.signer is not None: + return False + # If the brother wasn't recognized, don't try nagging + if not a.assignee.is_valid(): + return False + return True + + +class NotifyJobs(slack_util.Passive, JobNotifier): + async def run(self) -> None: + while True: + # Get the "Start" of the current day (Say, 10AM) + today_remind_time = datetime.now().replace(hour=10, minute=00, second=0) + + # Sleep until that time + delay = seconds_until(today_remind_time) + await asyncio.sleep(delay) + + # Now it is that time. Nag the jobs + job_commands.nag_jobs(self.get_day_of_week()) + + # Sleep for a bit to prevent double shots + await asyncio.sleep(10) + + +class RemindJobs(slack_util.Passive, JobNotifier): async def run(self) -> None: while True: # Get the end of the current day (Say, 10PM) today_remind_time = datetime.now().replace(hour=22, minute=00, second=0) - # Get the current day of week - dow = ["Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday"][datetime.now().weekday()] - # Sleep until that time delay = seconds_until(today_remind_time) await asyncio.sleep(delay) @@ -53,28 +97,10 @@ class RemindJobs(slack_util.Passive): assigns = await house_management.import_assignments() # Filter to incomplete, and today - def valid_filter(a: Optional[house_management.JobAssignment]): - # If it doesn't exist, it a thot - if a is None: - return False - # If its not today, we shouldn't nag - if a.job.day_of_week.lower() != dow.lower(): - return False - # If it is unassigned, we can't nag - if a.assignee is None: - return False - # If its been signed off, no need to nag - if a.signer is not None: - return False - # If the brother wasn't recognized, don't try nagging - if not a.assignee.is_valid(): - return False - return True - - assigns: List[house_management.JobAssignment] = [a for a in assigns if valid_filter(a)] + 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. - print("Nagging!") + print("Reminding!") for a in assigns: # Get the relevant slack ids assignee_ids = await identifier.lookup_brother_userids(a.assignee) @@ -92,3 +118,15 @@ class RemindJobs(slack_util.Passive): # Take a break to ensure no double-shots await asyncio.sleep(10) + + +class Updatinator(slack_util.Passive): + def __init__(self, wrapper_to_update: slack_util.ClientWrapper, interval_seconds: int): + self.wrapper_target = wrapper_to_update + self.interval = interval_seconds + + async def run(self): + while True: + await asyncio.sleep(self.interval) + self.wrapper_target.update_channels() + self.wrapper_target.update_users() diff --git a/slack_util.py b/slack_util.py index d206b9c..faf4c49 100644 --- a/slack_util.py +++ b/slack_util.py @@ -27,7 +27,7 @@ Objects to represent things within a slack workspace class User: id: str name: str - real_name: str + real_name: Optional[str] email: Optional[str] async def get_brother(self) -> Optional[scroll_util.Brother]: @@ -41,8 +41,6 @@ class User: class Channel: id: str name: str - purpose: str - members: List[User] """ @@ -109,7 +107,7 @@ def message_stream(slack: SlackClient) -> Generator[Event, None, None]: # Do forever while True: try: - if slack.rtm_connect(with_team_state=True, auto_reconnect=True): + if slack.rtm_connect(with_team_state=False, auto_reconnect=True): print("Waiting for messages") while True: sleep(0.1) @@ -171,6 +169,8 @@ class ClientWrapper(object): # Cache users and channels self.users: dict = {} self.channels: dict = {} + self.update_users() + self.update_channels() # Scheduled events handling def add_passive(self, per: Passive) -> None: @@ -281,6 +281,83 @@ class ClientWrapper(object): return self.api_call("chat.postMessage", **kwargs) + def update_channels(self): + """ + Queries the slack API for all current channels + """ + # Necessary because of pagination + cursor = None + + # Make a new dict to use + new_dict = {} + + # Iterate over results + while True: + # Set args depending on if a cursor exists + args = {"limit": 1000, "type": "public_channel,private_channel,mpim,im"} + if cursor: + args["cursor"] = cursor + + channel_dicts = self.api_call("https://slack.com/api/conversations.list", **args) + + # If the response is good, put its results to the dict + if channel_dicts["ok"]: + for channel_dict in channel_dicts["channels"]: + new_channel = Channel(id=channel_dict["id"], + name=channel_dict["name"]) + new_dict[new_channel.id] = new_channel + + # If no new channels, just give it up + if len(channel_dicts["channels"]) == 0: + break + + # Otherwise, fetch the cursor + cursor = channel_dicts.get("response_metadata").get("next_cursor") + + else: + print("Warning: failed to retrieve channels") + break + self.channels = new_dict + + def update_users(self): + """ + Queries the slack API for all current users + """ + # Necessary because of pagination + cursor = None + + while True: + # Set args depending on if a cursor exists + args = {"limit": 1000} + if cursor: + args["cursor"] = cursor + + user_dicts = self.api_call("https://slack.com/api/users.list", **args) + + # Make a new dict to use + new_dict = {} + + # If the response is good: + if user_dicts["ok"]: + for user_dict in user_dicts["members"]: + new_user = User(id=user_dict.get("id"), + name=user_dict.get("name"), + real_name=user_dict.get("real_name"), + email=user_dict.get("profile").get("email")) + new_dict[new_user.id] = new_user + + # If no new channels, just give it up + if len(user_dicts["channels"]) == 0: + break + + # Otherwise, fetch the cursor + cursor = user_dicts.get("response_metadata").get("next_cursor") + + else: + print("Warning: failed to retrieve channels") + break + self.users = new_dict + # Create a single instance of the client wrapper _singleton = ClientWrapper(SLACK_API) diff --git a/slavestothemachine.py b/slavestothemachine.py index 5034130..fcfc5b6 100644 --- a/slavestothemachine.py +++ b/slavestothemachine.py @@ -2,10 +2,7 @@ import re import textwrap from typing import Match -from slackclient import SlackClient - import house_management -import identifier import slack_util from scroll_util import Brother