Пример #1
0
 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
Пример #2
0
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.
    """))
Пример #3
0
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)
Пример #4
0
    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))
Пример #5
0
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)
Пример #6
0
    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()))
Пример #7
0
        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)
Пример #8
0
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)
Пример #9
0
        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.")
Пример #10
0
    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()))
Пример #11
0
    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)
Пример #12
0
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)
Пример #13
0
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))
Пример #14
0
    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)
Пример #15
0
    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)
Пример #16
0
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)
Пример #17
0
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)
Пример #18
0
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) + "```")
Пример #19
0
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
Пример #20
0
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)
Пример #21
0
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")
Пример #22
0
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)
Пример #23
0
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")
Пример #24
0
 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, [])
Пример #25
0
 def on_expire():
     # Edit the message to show defeat.
     client.get_slack().edit_message("Timed out", botzone.id, msg_ts, [])
Пример #26
0
 def get_user(self) -> Optional[User]:
     """
     Lookup the user to which this DM corresponds.
     """
     return client.get_slack().get_user(self.user_id)
Пример #27
0
 def get_conversation(self) -> Optional[Conversation]:
     return client.get_slack().get_conversation(self.conversation_id)
Пример #28
0
 def as_user(self) -> Optional[User]:
     return client.get_slack().get_user(self.user_id)
Пример #29
0
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)
Пример #30
0
async def reboot_callback(event: slack_util.Event, match: Match) -> None:
    response = "Ok. Rebooting..."
    client.get_slack().reply(event, response)
    exit(0)