Almost done

This commit is contained in:
Jacob Henry 2019-02-22 04:34:25 -05:00
parent fe33a5a0e4
commit ddb6ffac10
10 changed files with 460 additions and 435 deletions

View File

@ -1,31 +0,0 @@
from typing import Match
from slackclient import SlackClient
import slack_util
# Useful channels
GENERAL = "C0CFHPNEM"
RANDOM = "C0CFDQWUW"
COMMAND_CENTER_ID = "GCR631LQ1"
SLAVES_TO_THE_MACHINE_ID = "C9WUQBYNP"
BOTZONE = "C3BF2MFKM"
HOUSEJOBS = "CDWDDTAT0"
# Callback for telling what channel we in
async def channel_check_callback(slack: SlackClient, msg: dict, match: Match) -> None:
# Sets the users scroll
rest_of_msg = match.group(1).strip()
rest_of_msg = rest_of_msg.replace("<", "lcaret")
rest_of_msg = rest_of_msg.replace(">", "rcaret")
# Respond
response = ""
response += "Channel id: {}\n".format(msg["channel"])
response += "Escaped message: {}\n".format(rest_of_msg)
slack_util.reply(slack, msg, response)
channel_check_hook = slack_util.ChannelHook(channel_check_callback,
patterns=r"channel id\s*(.*)")

View File

@ -1,127 +0,0 @@
import asyncio
import traceback
from typing import List, Any, AsyncGenerator, Coroutine, TypeVar
from slackclient import SlackClient # Obvious
import channel_util
import sys
import slack_util
# Read the API token
api_file = open("apitoken.txt", 'r')
SLACK_API = next(api_file).strip()
api_file.close()
# Enable to do single-threaded and have better exceptions
DEBUG_MODE = False
A, B, C = TypeVar("A"), TypeVar("B"), TypeVar("C")
async def _loud_mouth(c: Coroutine[A, B, C]) -> Coroutine[A, B, C]:
# Print exceptions as they pass through
try:
return await c
except Exception:
traceback.print_exc()
raise
class ClientWrapper(object):
"""
Essentially the main state object.
We only ever expect one of these.
Holds a slack client, and handles messsages.
"""
def __init__(self):
# Init slack
self.slack = SlackClient(SLACK_API)
# Hooks go regex -> callback on (slack, msg, match)
self.hooks: List[slack_util.AbsHook] = []
# Periodicals are just wrappers around an iterable, basically
self.passives: List[slack_util.Passive] = []
# Scheduled events handling
def add_passive(self, per: slack_util.Passive) -> None:
self.passives.append(per)
async def run_passives(self) -> None:
# Make a task to repeatedly spawn each event
awaitables = [p.run(self.slack) for p in self.passives]
await asyncio.gather(*awaitables)
# Message handling
def add_hook(self, hook: slack_util.AbsHook) -> None:
self.hooks.append(hook)
async def respond_messages(self) -> None:
"""
Asynchronous tasks that eternally reads and responds to messages.
"""
async for t in self.spool_tasks():
sys.stdout.flush()
if DEBUG_MODE:
await t
async def spool_tasks(self) -> AsyncGenerator[asyncio.Task, Any]:
async for msg in self.async_message_feed():
# Preprocess msg
# We only care about standard messages, not subtypes, as those usually just channel activity
if msg.get("subtype") is not None:
continue
# Never deal with general, EVER!
if msg.get("channel") == channel_util.GENERAL:
continue
# Strip garbage
msg['text'] = msg['text'].strip()
print("Recv: \"{}\"".format(msg['text']))
print(msg)
# Msg is good
# Find which hook, if any, satisfies
for hook in self.hooks:
# Try invoking each
try:
# Try to make a coroutine handling the message
coro = hook.try_apply(self.slack, msg)
# If we get a coro back, then task it up and set consumption appropriately
if coro is not None:
print("Spawned task")
yield asyncio.create_task(_loud_mouth(coro))
if hook.consumes:
break
except slack_util.DeadHook:
# If a hook wants to die, let it.
self.hooks.remove(hook)
print("Done spawning tasks. Now {} running total.".format(len(asyncio.all_tasks())))
async def async_message_feed(self) -> AsyncGenerator[dict, None]:
"""
Async wrapper around the message feed.
Yields messages awaitably forever.
"""
# Create the msg feed
feed = slack_util.message_stream(self.slack)
# Create a simple callable that gets one message from the feed
def get_one():
return next(feed)
# Continuously yield async threaded tasks that poll the feed
while True:
yield await asyncio.get_running_loop().run_in_executor(None, get_one)
_singleton = ClientWrapper()
def grab() -> ClientWrapper:
return _singleton

View File

@ -1,17 +1,16 @@
""" """
Allows users to register their user account as a specific scroll Allows users to register their user account as a specific scroll
""" """
import asyncio
import shelve import shelve
from typing import List, Match from typing import List, Match
from slackclient import SlackClient
import scroll_util import scroll_util
import slack_util import slack_util
# The following db maps SLACK_USER_ID -> SCROLL_INTEGER # The following db maps SLACK_USER_ID -> SCROLL_INTEGER
DB_NAME = "user_scrolls" DB_NAME = "user_scrolls"
DB_LOCK = asyncio.Lock()
# Initialize the hooks # Initialize the hooks
NON_REG_MSG = ("You currently have no scroll registered. To register, type\n" NON_REG_MSG = ("You currently have no scroll registered. To register, type\n"
@ -19,16 +18,17 @@ NON_REG_MSG = ("You currently have no scroll registered. To register, type\n"
"except with your scroll instead of 666") "except with your scroll instead of 666")
async def identify_callback(slack, msg, match): async def identify_callback(event: slack_util.Event, match: Match):
""" """
Sets the users scroll Sets the users scroll
""" """
async with DB_LOCK:
with shelve.open(DB_NAME) as db: with shelve.open(DB_NAME) as db:
# Get the query # Get the query
query = match.group(1).strip() query = match.group(1).strip()
try: try:
user = msg.get("user") user = event.user.user_id
scroll = int(query) scroll = int(query)
db[user] = scroll db[user] = scroll
result = "Updated user {} to have scroll {}".format(user, scroll) result = "Updated user {} to have scroll {}".format(user, scroll)
@ -36,13 +36,14 @@ async def identify_callback(slack, msg, match):
result = "Bad scroll: {}".format(query) result = "Bad scroll: {}".format(query)
# Respond # Respond
slack_util.reply(slack, msg, result) slack_util.get_slack().reply(event, result)
async def identify_other_callback(slack: SlackClient, msg: dict, match: Match): async def identify_other_callback(event: slack_util.Event, match: Match):
""" """
Sets another users scroll Sets another users scroll
""" """
async with DB_LOCK:
with shelve.open(DB_NAME) as db: with shelve.open(DB_NAME) as db:
# Get the query # Get the query
user = match.group(1).strip() user = match.group(1).strip()
@ -59,32 +60,34 @@ async def identify_other_callback(slack: SlackClient, msg: dict, match: Match):
result = "Bad scroll: {}".format(scroll_txt) result = "Bad scroll: {}".format(scroll_txt)
# Respond # Respond
slack_util.reply(slack, msg, result) slack_util.get_slack().reply(event, result)
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
async def check_callback(slack: SlackClient, msg: dict, match: Match): async def check_callback(event: slack_util.Event, match: Match):
""" """
Replies with the users current scroll assignment Replies with the users current scroll assignment
""" """
async with DB_LOCK:
# Tells the user their current scroll # Tells the user their current scroll
with shelve.open(DB_NAME) as db: with shelve.open(DB_NAME) as db:
try: try:
scroll = db[msg.get("user")] scroll = db[event.user.user_id]
result = "You are currently registered with scroll {}".format(scroll) result = "You are currently registered with scroll {}".format(scroll)
except KeyError: except KeyError:
result = NON_REG_MSG result = NON_REG_MSG
slack_util.reply(slack, msg, result) slack_util.get_slack().reply(event, result)
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
async def name_callback(slack, msg, match): async def name_callback(event: slack_util.Event, match: Match):
""" """
Tells the user what it thinks the calling users name is. Tells the user what it thinks the calling users name is.
""" """
async with DB_LOCK:
with shelve.open(DB_NAME) as db: with shelve.open(DB_NAME) as db:
try: try:
scroll = db[msg.get("user")] scroll = db[event.user.user_id]
brother = scroll_util.find_by_scroll(scroll) brother = scroll_util.find_by_scroll(scroll)
if brother: if brother:
result = "The bot thinks your name is {}".format(brother.name) result = "The bot thinks your name is {}".format(brother.name)
@ -94,17 +97,7 @@ async def name_callback(slack, msg, match):
result = NON_REG_MSG result = NON_REG_MSG
# Respond # Respond
slack_util.reply(slack, msg, result) slack_util.get_slack().reply(event, result)
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 await lookup_slackid_brother(msg.get("user"))
async def lookup_slackid_brother(slack_id: str) -> scroll_util.Brother: async def lookup_slackid_brother(slack_id: str) -> scroll_util.Brother:
@ -113,6 +106,7 @@ async def lookup_slackid_brother(slack_id: str) -> scroll_util.Brother:
:raises BrotherNotFound: :raises BrotherNotFound:
:return: Brother object or None :return: Brother object or None
""" """
async with DB_LOCK:
with shelve.open(DB_NAME) as db: with shelve.open(DB_NAME) as db:
try: try:
scroll = db[slack_id] scroll = db[slack_id]
@ -121,13 +115,14 @@ async def lookup_slackid_brother(slack_id: str) -> scroll_util.Brother:
raise scroll_util.BrotherNotFound("Slack id {} not tied to brother".format(slack_id)) raise scroll_util.BrotherNotFound("Slack id {} not tied to brother".format(slack_id))
def lookup_brother_userids(brother: scroll_util.Brother) -> List[str]: async def lookup_brother_userids(brother: scroll_util.Brother) -> List[str]:
""" """
Returns a list of all userids associated with the given brother. Returns a list of all userids associated with the given brother.
:param brother: Brother to lookup scrolls for :param brother: Brother to lookup scrolls for
:return: List of user id strings (may be empty) :return: List of user id strings (may be empty)
""" """
async with DB_LOCK:
with shelve.open(DB_NAME) as db: with shelve.open(DB_NAME) as db:
keys = db.keys() keys = db.keys()
result = [] result = []

View File

@ -4,8 +4,6 @@ from typing import List, Match, Callable, TypeVar, Optional, Iterable
from fuzzywuzzy import fuzz from fuzzywuzzy import fuzz
from slackclient import SlackClient from slackclient import SlackClient
import channel_util
import client_wrapper
import house_management import house_management
import identifier import identifier
import scroll_util import scroll_util
@ -16,14 +14,14 @@ SHEET_ID = "1lPj9GjB00BuIq9GelOWh5GmiGsheLlowPnHLnWBvMOM"
MIN_RATIO = 80.0 MIN_RATIO = 80.0
def alert_user(slack: SlackClient, brother: scroll_util.Brother, saywhat: str) -> None: async def alert_user(brother: scroll_util.Brother, saywhat: str) -> None:
""" """
DM a brother saying something. Wrapper around several simpler methods 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) # 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 succ = False
for slack_id in identifier.lookup_brother_userids(brother): for slack_id in await identifier.lookup_brother_userids(brother):
slack_util.send_message(slack, saywhat, slack_id) slack_util.get_slack().send_message(saywhat, slack_id)
succ = True succ = True
# Warn if we never find # Warn if we never find
@ -31,6 +29,7 @@ def alert_user(slack: SlackClient, brother: scroll_util.Brother, saywhat: str) -
print("Warning: unable to find dm for brother {}".format(brother)) print("Warning: unable to find dm for brother {}".format(brother))
# Generic type
T = TypeVar("T") T = TypeVar("T")
@ -60,8 +59,7 @@ class _ModJobContext:
assign: house_management.JobAssignment # The job assignment to modify assign: house_management.JobAssignment # The job assignment to modify
async def _mod_jobs(slack: SlackClient, async def _mod_jobs(event: slack_util.Event,
msg: dict,
relevance_scorer: Callable[[house_management.JobAssignment], Optional[float]], relevance_scorer: Callable[[house_management.JobAssignment], Optional[float]],
modifier: Callable[[_ModJobContext], None], modifier: Callable[[_ModJobContext], None],
no_job_msg: str = None no_job_msg: str = None
@ -72,10 +70,10 @@ async def _mod_jobs(slack: SlackClient,
:param modifier: Callback function to modify a job. Only called on a successful operation, and only on one job :param modifier: Callback function to modify a job. Only called on a successful operation, and only on one job
""" """
# Make an error wrapper # Make an error wrapper
verb = slack_util.VerboseWrapper(slack, msg) verb = slack_util.VerboseWrapper(event)
# Who invoked this command? # Who invoked this command?
signer = await verb(identifier.lookup_msg_brother(msg)) signer = await verb(event.user.as_user().get_brother())
# Get all of the assignments # Get all of the assignments
assigns = await verb(house_management.import_assignments()) assigns = await verb(house_management.import_assignments())
@ -115,7 +113,7 @@ async def _mod_jobs(slack: SlackClient,
if len(closest_assigns) == 0: if len(closest_assigns) == 0:
if no_job_msg is None: 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." 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) slack_util.get_slack().reply(event, no_job_msg)
# If theres only one job, sign it off # If theres only one job, sign it off
elif len(closest_assigns) == 1: elif len(closest_assigns) == 1:
@ -125,7 +123,7 @@ async def _mod_jobs(slack: SlackClient,
else: else:
# Say we need more info # Say we need more info
job_list = "\n".join("{}: {}".format(i, a.job.pretty_fmt()) for i, a in enumerate(closest_assigns)) job_list = "\n".join("{}: {}".format(i, a.job.pretty_fmt()) for i, a in enumerate(closest_assigns))
slack_util.reply(slack, msg, "Multiple relevant job listings found.\n" slack_util.get_slack().reply(event, "Multiple relevant job listings found.\n"
"Please enter the number corresponding to the job " "Please enter the number corresponding to the job "
"you wish to modify:\n{}".format(job_list)) "you wish to modify:\n{}".format(job_list))
@ -133,7 +131,7 @@ async def _mod_jobs(slack: SlackClient,
pattern = r"\d+" pattern = r"\d+"
# Make the follow up callback # Make the follow up callback
async def foc(_slack: SlackClient, _msg: dict, _match: Match) -> None: async def foc(_event: slack_util.Event, _match: Match) -> None:
# Get the number out # Get the number out
index = int(_match.group(0)) index = int(_match.group(0))
@ -143,17 +141,17 @@ async def _mod_jobs(slack: SlackClient,
await success_callback(closest_assigns[index]) await success_callback(closest_assigns[index])
else: else:
# They gave a bad index, or we were unable to find the assignment again. # 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.") slack_util.get_slack().reply(_event, "Invalid job index / job unable to be found.")
# Make a listener hook # Make a listener hook
new_hook = slack_util.ReplyWaiter(foc, pattern, msg["ts"], 120) new_hook = slack_util.ReplyWaiter(foc, pattern, event.message.ts, 120)
# Register it # Register it
client_wrapper.grab().add_hook(new_hook) slack_util.get_slack().add_hook(new_hook)
async def signoff_callback(slack: SlackClient, msg: dict, match: Match) -> None: async def signoff_callback(event: slack_util.Event, match: Match) -> None:
verb = slack_util.VerboseWrapper(slack, msg) verb = slack_util.VerboseWrapper(event)
# Find out who we are trying to sign off is # Find out who we are trying to sign off is
signee_name = match.group(1) signee_name = match.group(1)
@ -171,17 +169,17 @@ async def signoff_callback(slack: SlackClient, msg: dict, match: Match) -> None:
context.assign.signer = context.signer context.assign.signer = context.signer
# Say we did it wooo! # Say we did it wooo!
slack_util.reply(slack, msg, "Signed off {} for {}".format(context.assign.assignee.name, slack_util.get_slack().reply(event, "Signed off {} for {}".format(context.assign.assignee.name,
context.assign.job.name)) context.assign.job.name))
alert_user(slack, context.assign.assignee, "{} signed you off for {}.".format(context.assign.signer.name, alert_user(context.assign.assignee, "{} signed you off for {}.".format(context.assign.signer.name,
context.assign.job.pretty_fmt())) context.assign.job.pretty_fmt()))
# Fire it off # Fire it off
await _mod_jobs(slack, msg, scorer, modifier) await _mod_jobs(event, scorer, modifier)
async def undo_callback(slack: SlackClient, msg: dict, match: Match) -> None: async def undo_callback(event: slack_util.Event, match: Match) -> None:
verb = slack_util.VerboseWrapper(slack, msg) verb = slack_util.VerboseWrapper(event)
# Find out who we are trying to sign off is # Find out who we are trying to sign off is
signee_name = match.group(1) signee_name = match.group(1)
@ -199,18 +197,18 @@ async def undo_callback(slack: SlackClient, msg: dict, match: Match) -> None:
context.assign.signer = None context.assign.signer = None
# Say we did it wooo! # Say we did it wooo!
slack_util.reply(slack, msg, "Undid signoff of {} for {}".format(context.assign.assignee.name, 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(slack, context.assign.assignee, "{} undid your signoff off for {}.\n" alert_user(context.assign.assignee, "{} undid your signoff off for {}.\n"
"Must have been a mistake".format(context.assign.signer.name, "Must have been a mistake".format(context.assign.signer.name,
context.assign.job.pretty_fmt())) context.assign.job.pretty_fmt()))
# Fire it off # Fire it off
await _mod_jobs(slack, msg, scorer, modifier) await _mod_jobs(event, scorer, modifier)
async def late_callback(slack: SlackClient, msg: dict, match: Match) -> None: async def late_callback(event: slack_util.Event, match: Match) -> None:
verb = slack_util.VerboseWrapper(slack, msg) verb = slack_util.VerboseWrapper(event)
# Find out who we are trying to sign off is # Find out who we are trying to sign off is
signee_name = match.group(1) signee_name = match.group(1)
@ -228,16 +226,16 @@ async def late_callback(slack: SlackClient, msg: dict, match: Match) -> None:
context.assign.late = not context.assign.late context.assign.late = not context.assign.late
# Say we did it # Say we did it
slack_util.reply(slack, msg, "Toggled lateness of {}.\n" slack_util.get_slack().reply(event, "Toggled lateness of {}.\n"
"Now marked as late: {}".format(context.assign.job.pretty_fmt(), "Now marked as late: {}".format(context.assign.job.pretty_fmt(),
context.assign.late)) context.assign.late))
# Fire it off # Fire it off
await _mod_jobs(slack, msg, scorer, modifier) await _mod_jobs(event, scorer, modifier)
async def reassign_callback(slack: SlackClient, msg: dict, match: Match) -> None: async def reassign_callback(event: slack_util.Event, match: Match) -> None:
verb = slack_util.VerboseWrapper(slack, msg) verb = slack_util.VerboseWrapper(event)
# Find out our two targets # Find out our two targets
from_name = match.group(1).strip() from_name = match.group(1).strip()
@ -263,21 +261,21 @@ async def reassign_callback(slack: SlackClient, msg: dict, match: Match) -> None
reassign_msg = "Job {} reassigned from {} to {}".format(context.assign.job.pretty_fmt(), reassign_msg = "Job {} reassigned from {} to {}".format(context.assign.job.pretty_fmt(),
from_bro, from_bro,
to_bro) to_bro)
slack_util.reply(slack, msg, reassign_msg) slack_util.get_slack().reply(event, reassign_msg)
# Tell the people # Tell the people
reassign_msg = "Job {} reassigned from {} to {}".format(context.assign.job.pretty_fmt(), reassign_msg = "Job {} reassigned from {} to {}".format(context.assign.job.pretty_fmt(),
from_bro, from_bro,
to_bro) to_bro)
alert_user(slack, from_bro, reassign_msg) alert_user(from_bro, reassign_msg)
alert_user(slack, to_bro, reassign_msg) alert_user(to_bro, reassign_msg)
# Fire it off # Fire it off
await _mod_jobs(slack, msg, scorer, modifier) await _mod_jobs(event, scorer, modifier)
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
async def reset_callback(slack: SlackClient, msg: dict, match: Match) -> None: async def reset_callback(event: slack_util.Event, match: Match) -> None:
""" """
Resets the scores. Resets the scores.
""" """
@ -299,18 +297,18 @@ async def reset_callback(slack: SlackClient, msg: dict, match: Match) -> None:
house_management.apply_house_points(points, await house_management.import_assignments()) house_management.apply_house_points(points, await house_management.import_assignments())
house_management.export_points(headers, points) house_management.export_points(headers, points)
slack_util.reply(slack, msg, "Reset scores and signoffs") slack_util.get_slack().reply(event, "Reset scores and signoffs")
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
async def refresh_callback(slack: SlackClient, msg: dict, match: Match) -> None: async def refresh_callback(event: slack_util.Event, match: Match) -> None:
headers, points = await house_management.import_points() headers, points = await house_management.import_points()
house_management.apply_house_points(points, await house_management.import_assignments()) house_management.apply_house_points(points, await house_management.import_assignments())
house_management.export_points(headers, points) house_management.export_points(headers, points)
slack_util.reply(slack, msg, "Force updated point values") slack_util.get_slack().reply(event, "Force updated point values")
async def nag_callback(slack, msg, match): async def nag_callback(event: slack_util.Event, match: Match) -> None:
# Get the day # Get the day
day = match.group(1).lower().strip() day = match.group(1).lower().strip()
@ -325,7 +323,7 @@ async def nag_callback(slack, msg, match):
# If no jobs found, somethings up. Probably mispelled day. # If no jobs found, somethings up. Probably mispelled day.
if not assigns: if not assigns:
slack_util.reply(slack, msg, "No jobs found. Check that the day is spelled correctly, with no extra symbols.\n" 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.", "It is possible that all jobs have been signed off, as well.",
in_thread=True) in_thread=True)
return return
@ -339,7 +337,7 @@ async def nag_callback(slack, msg, match):
response += "({}) {} -- {} ".format(assign.job.house, assign.job.name, assign.assignee.name) response += "({}) {} -- {} ".format(assign.job.house, assign.job.name, assign.assignee.name)
# Find the people to @ # Find the people to @
brother_slack_ids = identifier.lookup_brother_userids(assign.assignee) brother_slack_ids = await identifier.lookup_brother_userids(assign.assignee)
if brother_slack_ids: if brother_slack_ids:
for slack_id in brother_slack_ids: for slack_id in brother_slack_ids:
@ -348,7 +346,8 @@ async def nag_callback(slack, msg, match):
response += "(scroll missing. Please register for @ pings!)" response += "(scroll missing. Please register for @ pings!)"
response += "\n" response += "\n"
slack_util.reply(slack, msg, response, in_thread=False, to_channel=channel_util.GENERAL) 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)
signoff_hook = slack_util.ChannelHook(signoff_callback, signoff_hook = slack_util.ChannelHook(signoff_callback,
@ -356,7 +355,7 @@ signoff_hook = slack_util.ChannelHook(signoff_callback,
r"signoff\s+(.*)", r"signoff\s+(.*)",
r"sign off\s+(.*)", r"sign off\s+(.*)",
], ],
channel_whitelist=[channel_util.HOUSEJOBS]) channel_whitelist=["#housejobs"])
undo_hook = slack_util.ChannelHook(undo_callback, undo_hook = slack_util.ChannelHook(undo_callback,
patterns=[ patterns=[
@ -364,37 +363,36 @@ undo_hook = slack_util.ChannelHook(undo_callback,
r"undosignoff\s+(.*)", r"undosignoff\s+(.*)",
r"undo signoff\s+(.*)", r"undo signoff\s+(.*)",
], ],
channel_whitelist=[channel_util.HOUSEJOBS]) channel_whitelist=["#housejobs"])
late_hook = slack_util.ChannelHook(late_callback, late_hook = slack_util.ChannelHook(late_callback,
patterns=[ patterns=[
r"marklate\s+(.*)", r"marklate\s+(.*)",
r"mark late\s+(.*)", r"mark late\s+(.*)",
], ],
channel_whitelist=[channel_util.HOUSEJOBS]) channel_whitelist=["#housejobs"])
reset_hook = slack_util.ChannelHook(reset_callback, reset_hook = slack_util.ChannelHook(reset_callback,
patterns=[ patterns=[
r"reset signoffs", r"reset signoffs",
r"reset sign offs", r"reset sign offs",
], ],
channel_whitelist=[channel_util.COMMAND_CENTER_ID]) channel_whitelist=["#command-center"])
nag_hook = slack_util.ChannelHook(nag_callback, nag_hook = slack_util.ChannelHook(nag_callback,
patterns=[ patterns=[
r"nagjobs\s+(.*)", r"nagjobs\s+(.*)",
r"nag jobs\s+(.*)" r"nag jobs\s+(.*)"
], ],
channel_whitelist=[channel_util.COMMAND_CENTER_ID]) channel_whitelist=["#command-center"])
reassign_hook = slack_util.ChannelHook(reassign_callback, reassign_hook = slack_util.ChannelHook(reassign_callback,
patterns=r"reassign\s+(.*?)-&gt;\s+(.+)", patterns=r"reassign\s+(.*?)-&gt;\s+(.+)",
channel_whitelist=[channel_util.HOUSEJOBS]) channel_whitelist=["#housejobs"])
refresh_hook = slack_util.ChannelHook(refresh_callback, refresh_hook = slack_util.ChannelHook(refresh_callback,
patterns=[ patterns=[
"refresh points", "refresh points",
"update points" "update points"
], ],
channel_whitelist=[channel_util.COMMAND_CENTER_ID] channel_whitelist=["#command-center"])
)

15
main.py
View File

@ -2,10 +2,6 @@ import asyncio
import textwrap import textwrap
from typing import Match from typing import Match
from slackclient import SlackClient
import channel_util
import client_wrapper
import identifier import identifier
import job_commands import job_commands
import management_commands import management_commands
@ -16,7 +12,7 @@ import slavestothemachine
def main() -> None: def main() -> None:
wrap = client_wrapper.grab() wrap = slack_util.get_slack()
# Add scroll handling # Add scroll handling
wrap.add_hook(scroll_util.scroll_hook) wrap.add_hook(scroll_util.scroll_hook)
@ -27,9 +23,6 @@ def main() -> None:
wrap.add_hook(identifier.identify_other_hook) wrap.add_hook(identifier.identify_other_hook)
wrap.add_hook(identifier.name_hook) wrap.add_hook(identifier.name_hook)
# Added channel utility
wrap.add_hook(channel_util.channel_check_hook)
# Add kill switch # Add kill switch
wrap.add_hook(management_commands.reboot_hook) wrap.add_hook(management_commands.reboot_hook)
@ -55,7 +48,7 @@ def main() -> None:
wrap.add_passive(periodicals.RemindJobs()) wrap.add_passive(periodicals.RemindJobs())
event_loop = asyncio.get_event_loop() event_loop = asyncio.get_event_loop()
event_loop.set_debug(client_wrapper.DEBUG_MODE) event_loop.set_debug(slack_util.DEBUG_MODE)
message_handling = wrap.respond_messages() message_handling = wrap.respond_messages()
passive_handling = wrap.run_passives() passive_handling = wrap.run_passives()
both = asyncio.gather(message_handling, passive_handling) both = asyncio.gather(message_handling, passive_handling)
@ -63,8 +56,8 @@ def main() -> None:
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
async def help_callback(slack: SlackClient, msg: dict, match: Match) -> None: async def help_callback(event: slack_util.Event, match: Match) -> None:
slack_util.reply(slack, msg, textwrap.dedent(""" slack_util.get_slack().reply(event, textwrap.dedent("""
Commands are as follows. Note that some only work in certain channels. 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. "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. "@person has scroll number" : same as above, but for other users. Helpful if they are being obstinate.

View File

@ -1,28 +1,25 @@
from typing import Match, List from typing import Match, List
from slackclient import SlackClient
import channel_util
import slack_util import slack_util
def list_hooks_callback_gen(hooks: List[slack_util.ChannelHook]) -> slack_util.Callback: def list_hooks_callback_gen(hooks: List[slack_util.ChannelHook]) -> slack_util.Callback:
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
async def callback(slack, msg, match): async def callback(event: slack_util.Event, match: Match) -> None:
slack_util.reply(slack, msg, "\n".join(hook.patterns for hook in hooks)) slack_util.get_slack().reply(event, "\n".join(hook.patterns for hook in hooks))
return callback return callback
# Gracefully reboot to reload code changes # Gracefully reboot to reload code changes
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
async def reboot_callback(slack: SlackClient, msg: dict, match: Match) -> None: async def reboot_callback(event: slack_util.Event, match: Match) -> None:
response = "Ok. Rebooting..." response = "Ok. Rebooting..."
slack_util.reply(slack, msg, response) slack_util.get_slack().reply(event, response)
exit(0) exit(0)
# Make hooks # Make hooks
reboot_hook = slack_util.ChannelHook(reboot_callback, reboot_hook = slack_util.ChannelHook(reboot_callback,
patterns=r"reboot", patterns=r"reboot",
channel_whitelist=[channel_util.COMMAND_CENTER_ID]) channel_whitelist=["#command-center"])

View File

@ -2,9 +2,6 @@ import asyncio
from datetime import datetime from datetime import datetime
from typing import Optional, List from typing import Optional, List
from slackclient import SlackClient
import channel_util
import house_management import house_management
import identifier import identifier
import slack_util import slack_util
@ -17,7 +14,7 @@ def seconds_until(target: datetime) -> float:
class ItsTenPM(slack_util.Passive): class ItsTenPM(slack_util.Passive):
async def run(self, slack: SlackClient) -> None: async def run(self) -> None:
while True: while True:
# Get 10PM # Get 10PM
ten_pm = datetime.now().replace(hour=22, minute=0, second=0) ten_pm = datetime.now().replace(hour=22, minute=0, second=0)
@ -27,14 +24,14 @@ class ItsTenPM(slack_util.Passive):
await asyncio.sleep(delay) await asyncio.sleep(delay)
# Crow like a rooster # Crow like a rooster
slack_util.send_message(slack, "IT'S 10 PM!", channel_util.RANDOM) slack_util.get_slack().send_message("IT'S 10 PM!", slack_util.get_slack().get_channel_by_name("#random").id)
# Wait a while before trying it again, to prevent duplicates # Wait a while before trying it again, to prevent duplicates
await asyncio.sleep(60) await asyncio.sleep(60)
class RemindJobs(slack_util.Passive): class RemindJobs(slack_util.Passive):
async def run(self, slack: SlackClient) -> None: async def run(self) -> None:
while True: while True:
# Get the end of the current day (Say, 10PM) # Get the end of the current day (Say, 10PM)
today_remind_time = datetime.now().replace(hour=22, minute=00, second=0) today_remind_time = datetime.now().replace(hour=22, minute=00, second=0)
@ -80,14 +77,14 @@ class RemindJobs(slack_util.Passive):
print("Nagging!") print("Nagging!")
for a in assigns: for a in assigns:
# Get the relevant slack ids # Get the relevant slack ids
assignee_ids = identifier.lookup_brother_userids(a.assignee) assignee_ids = await identifier.lookup_brother_userids(a.assignee)
# For each, send them a DM # For each, send them a DM
success = False success = False
for slack_id in assignee_ids: for slack_id in assignee_ids:
msg = "{}, you still need to do {}".format(a.assignee.name, a.job.pretty_fmt()) msg = "{}, you still need to do {}".format(a.assignee.name, a.job.pretty_fmt())
success = True success = True
slack_util.send_message(slack, msg, slack_id) slack_util.get_slack().send_message(msg, slack_id)
# Warn on failure # Warn on failure
if not success: if not success:

View File

@ -8,7 +8,6 @@ from dataclasses import dataclass
from typing import List, Optional, Match from typing import List, Optional, Match
from fuzzywuzzy import process from fuzzywuzzy import process
from slackclient import SlackClient
import slack_util import slack_util
@ -38,7 +37,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] brothers: List[Brother] = [Brother(m.group(2), int(m.group(1))) for m in brothers_matches]
async def scroll_callback(slack: SlackClient, msg: dict, match: Match) -> None: 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. Finds the scroll of a brother, or the brother of a scroll, based on msg text.
""" """
@ -57,7 +56,7 @@ async def scroll_callback(slack: SlackClient, msg: dict, match: Match) -> None:
result = "Couldn't find brother {}".format(query) result = "Couldn't find brother {}".format(query)
# Respond # Respond
slack_util.reply(slack, msg, result) slack_util.get_slack().reply(event, result)
def find_by_scroll(scroll: int) -> Optional[Brother]: def find_by_scroll(scroll: int) -> Optional[Brother]:

View File

@ -1,71 +1,61 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import re import re
import sys
import traceback
from dataclasses import dataclass from dataclasses import dataclass
from time import sleep, time from time import sleep, time
from typing import Any, Optional, Generator, Match, Callable, List, Coroutine, Union, TypeVar, Awaitable from typing import List, Any, AsyncGenerator, Coroutine, TypeVar
from typing import Optional, Generator, Match, Callable, Union, Awaitable
from slackclient import SlackClient from slackclient import SlackClient
from slackclient.client import SlackNotConnected from slackclient.client import SlackNotConnected
# Enable to do single-threaded and have better exceptions
import identifier
import scroll_util
DEBUG_MODE = False
""" """
Slack helpers. Separated for compartmentalization Objects to represent things within a slack workspace
""" """
def reply(msg: dict, text: str, in_thread: bool = True, to_channel: str = None) -> dict: @dataclass
"""
Sends message with "text" as its content to the channel that message came from.
Returns the JSON response.
"""
# If no channel specified, just do same as msg
if to_channel is None:
to_channel = msg['channel']
# Send in a thread by default
if in_thread:
thread = (msg.get("thread_ts") # In-thread case - get parent ts
or msg.get("ts")) # Not in-thread case - get msg itself ts
return send_message(slack, text, to_channel, thread=thread)
else:
return send_message(slack, text, to_channel)
def send_message(text: str, channel: str, thread: str = None, broadcast: bool = False) -> dict:
"""
Copy of the internal send message function of slack, with some helpful options.
Returns the JSON response.
"""
kwargs = {"channel": channel, "text": text}
if thread:
kwargs["thread_ts"] = thread
if broadcast:
kwargs["reply_broadcast"] = True
return slack.api_call("chat.postMessage", **kwargs)
"""
Objects to represent things
"""
class User: class User:
pass id: str
name: str
real_name: str
email: Optional[str]
async def get_brother(self) -> Optional[scroll_util.Brother]:
"""
Try to find the brother corresponding to this user.
"""
return await identifier.lookup_slackid_brother(self.id)
@dataclass
class Channel: class Channel:
@property id: str
def channel_name(self) -> str: name: str
raise NotImplementedError() purpose: str
members: List[User]
""" """
Below we have a modular system that represents possible event contents. Objects to represent attributes an event may contain
""" """
@dataclass @dataclass
class Event: class Event:
channel: Optional[ChannelContext] channel: Optional[ChannelContext] = None
user: Optional[UserContext] user: Optional[UserContext] = None
message: Optional[Message] message: Optional[MessageContext] = None
thread: Optional[ThreadContext] = None
# If this was posted in a specific channel or conversation # If this was posted in a specific channel or conversation
@ -73,6 +63,9 @@ class Event:
class ChannelContext: class ChannelContext:
channel_id: str channel_id: str
def get_channel(self) -> Channel:
raise NotImplementedError()
# If there is a specific user associated with this event # If there is a specific user associated with this event
@dataclass @dataclass
@ -85,16 +78,28 @@ class UserContext:
# Whether or not this is a threadable text message # Whether or not this is a threadable text message
@dataclass @dataclass
class Message: class MessageContext:
ts: str ts: str
text: str text: str
@dataclass
class ThreadContext:
thread_ts: str
parent_ts: str
# If a file was additionally shared
@dataclass @dataclass
class File: class File:
pass pass
"""
Objects for interfacing easily with rtm steams
"""
def message_stream(slack: SlackClient) -> Generator[Event, None, None]: def message_stream(slack: SlackClient) -> Generator[Event, None, None]:
""" """
Generator that yields messages from slack. Generator that yields messages from slack.
@ -104,50 +109,199 @@ def message_stream(slack: SlackClient) -> Generator[Event, None, None]:
# Do forever # Do forever
while True: while True:
try: try:
if slack.rtm_connect(with_team_state=False, auto_reconnect=True): if slack.rtm_connect(with_team_state=True, auto_reconnect=True):
print("Waiting for messages") print("Waiting for messages")
while True: while True:
sleep(0.1) sleep(0.1)
update = slack.rtm_read() update_list = slack.rtm_read()
for item in update:
if item.get('type') == 'message': # Handle each
yield item for update in update_list:
print("Message received: {}".format(update))
event = Event()
# Big logic folks
if update["type"] == "message":
event.message = MessageContext(update["ts"], update["text"])
event.channel = ChannelContext(update["channel"])
event.user = UserContext(update["user"])
# TODO: Handle more types
# We need to
yield event
except (SlackNotConnected, OSError) as e: except (SlackNotConnected, OSError) as e:
print("Error while reading messages:") print("Error while reading messages:")
print(e) print(e)
except (ValueError, TypeError): except (ValueError, TypeError) as e:
print("Malformed message... Restarting connection") print("Malformed message... Restarting connection")
print(e)
sleep(5) sleep(5)
print("Connection failed - retrying") print("Connection failed - retrying")
T = TypeVar("T")
class VerboseWrapper(Callable):
""" """
Generates exception-ready delegates. Objects to wrap slack connections
Warns of exceptions as they are passed through it, via responding to the given message.
""" """
def __init__(self, slack: SlackClient, command_msg: dict): # Read the API token
self.slack = slack api_file = open("apitoken.txt", 'r')
self.command_msg = command_msg SLACK_API = next(api_file).strip()
api_file.close()
async def __call__(self, awt: Awaitable[T]) -> T:
class ClientWrapper(object):
"""
Essentially the main state object.
We only ever expect one of these per api token.
Holds a slack client, and handles messsages.
"""
def __init__(self, api_token):
# Init slack
self.slack = SlackClient(api_token)
# Hooks go regex -> callback on (slack, msg, match)
self.hooks: List[AbsHook] = []
# Periodicals are just wrappers around an iterable, basically
self.passives: List[Passive] = []
# Cache users and channels
self.users: dict = {}
self.channels: dict = {}
# Scheduled events handling
def add_passive(self, per: Passive) -> None:
self.passives.append(per)
async def run_passives(self) -> None:
"""
Run all currently added passives
"""
awaitables = [p.run() for p in self.passives]
await asyncio.gather(*awaitables)
# Message handling
def add_hook(self, hook: AbsHook) -> None:
self.hooks.append(hook)
async def respond_messages(self) -> None:
"""
Asynchronous tasks that eternally reads and responds to messages.
"""
async for t in self.spool_tasks():
sys.stdout.flush()
if DEBUG_MODE:
await t
async def spool_tasks(self) -> AsyncGenerator[asyncio.Task, Any]:
async for event in self.async_event_feed():
# Find which hook, if any, satisfies
for hook in list(self.hooks): # Note that we do list(self.hooks) to avoid edit-while-iterating issues
# Try invoking each
try: try:
return await awt # Try to make a coroutine handling the message
except Exception as e: coro = hook.try_apply(event)
reply(self.command_msg, "Error: {}".format(str(e)), True)
raise e # If we get a coro back, then task it up and set consumption appropriately
if coro is not None:
print("Spawned task")
yield asyncio.create_task(_exception_printing_task(coro))
if hook.consumes:
break
except DeadHook:
# If a hook wants to die, let it.
self.hooks.remove(hook)
print("Done spawning tasks. Now {} running total.".format(len(asyncio.all_tasks())))
async def async_event_feed(self) -> AsyncGenerator[Event, None]:
"""
Async wrapper around the message feed.
Yields messages awaitably forever.
"""
# Create the msg feed
feed = message_stream(self.slack)
# Create a simple callable that gets one message from the feed
def get_one():
return next(feed)
# Continuously yield async threaded tasks that poll the feed
while True:
yield await asyncio.get_running_loop().run_in_executor(None, get_one)
def get_channel(self, channel_id: str) -> Optional[Channel]:
return self.channels.get(channel_id)
def get_channel_by_name(self, channel_name: str) -> Optional[Channel]:
# Find the channel in the dict
for v in self.channels.values():
if v.name == channel_name:
return v
return None
def get_user(self, user_id: str) -> Optional[Channel]:
return self.users.get(user_id)
def api_call(self, api_method, **kwargs):
return self.slack.api_call(api_method, **kwargs)
def reply(self, event: Event, text: str, in_thread: bool = True) -> dict:
"""
Replies to a message.
Message must have a channel and message context.
Returns the JSON response.
"""
# Ensure we're actually replying to a valid message
assert (event.channel and event.message) is not None
# Send in a thread by default
if in_thread:
# Figure otu what thread to send it to
thread = event.message.ts
if event.thread:
thread = event.thread.thread_ts
return self.send_message(text, event.channel.channel_id, thread=thread)
else:
return self.send_message(text, event.channel.channel_id)
def send_message(self, text: str, channel_id: str, thread: str = None, broadcast: bool = False) -> dict:
"""
Copy of the internal send message function of slack, with some helpful options.
Returns the JSON response.
"""
kwargs = {"channel": channel_id, "text": text}
if thread:
kwargs["thread_ts"] = thread
if broadcast:
kwargs["reply_broadcast"] = True
return self.api_call("chat.postMessage", **kwargs)
# The result of a message # Create a single instance of the client wrapper
_singleton = ClientWrapper(SLACK_API)
def get_slack() -> ClientWrapper:
return _singleton
# Return type of an event callback
MsgAction = Coroutine[Any, Any, None] MsgAction = Coroutine[Any, Any, None]
# The function called on a message
Callback = Callable[[SlackClient, Message, Match], MsgAction] # Type signature of an event callback function
Callback = Callable[[Event, Match], MsgAction]
"""
Hooks
"""
# Signal exception to be raised when a hook has died
class DeadHook(Exception): class DeadHook(Exception):
pass pass
@ -166,6 +320,7 @@ class ChannelHook(AbsHook):
""" """
Hook that handles messages in a variety of channels Hook that handles messages in a variety of channels
""" """
def __init__(self, def __init__(self,
callback: Callback, callback: Callback,
patterns: Union[str, List[str]], patterns: Union[str, List[str]],
@ -185,8 +340,7 @@ class ChannelHook(AbsHook):
# Remedy some sensible defaults # Remedy some sensible defaults
if self.channel_blacklist is None: if self.channel_blacklist is None:
import channel_util self.channel_blacklist = ["#general"]
self.channel_blacklist = [channel_util.GENERAL]
elif self.channel_whitelist is None: elif self.channel_whitelist is None:
pass # We leave as none to show no whitelisting in effect pass # We leave as none to show no whitelisting in effect
else: else:
@ -196,25 +350,32 @@ class ChannelHook(AbsHook):
""" """
Returns whether a message should be handled by this dict, returning a Match if so, or None Returns whether a message should be handled by this dict, returning a Match if so, or None
""" """
# Ensure that this is an event in a specific channel, with a text component
if not (event.channel and event.message):
return None
# Fail if pattern invalid # Fail if pattern invalid
match = None match = None
for p in self.patterns: for p in self.patterns:
match = re.match(p, msg['text'], flags=re.IGNORECASE) match = re.match(p, event.message.text.strip(), flags=re.IGNORECASE)
if match is not None: if match is not None:
break break
if match is None: if match is None:
return None return None
# Get the channel name
channel_name = event.channel.get_channel().name
# Fail if whitelist defined, and we aren't there # Fail if whitelist defined, and we aren't there
if self.channel_whitelist is not None and msg["channel"] not in self.channel_whitelist: if self.channel_whitelist is not None and channel_name not in self.channel_whitelist:
return None return None
# Fail if blacklist defined, and we are there # Fail if blacklist defined, and we are there
if self.channel_blacklist is not None and msg["channel"] in self.channel_blacklist: if self.channel_blacklist is not None and channel_name in self.channel_blacklist:
return None return None
return self.callback(slack, msg, match) return self.callback(event, match)
class ReplyWaiter(AbsHook): class ReplyWaiter(AbsHook):
@ -238,19 +399,22 @@ class ReplyWaiter(AbsHook):
# If so, give up the ghost # If so, give up the ghost
if self.dead or should_expire: if self.dead or should_expire:
print("Reply waiter has expired after {} seconds".format(time_alive))
raise DeadHook() raise DeadHook()
# Next make sure we're actually a message
if not (event.message and event.thread):
return None
# Otherwise proceed normally # Otherwise proceed normally
# Is the msg the one we care about? If not, ignore # Is the msg the one we care about? If not, ignore
if msg.get("thread_ts", None) != self.thread_ts: if event.thread.thread_ts != self.thread_ts:
return None return None
# Does it match the regex? if not, ignore # Does it match the regex? if not, ignore
match = re.match(self.pattern, msg['text'], flags=re.IGNORECASE) match = re.match(self.pattern, event.message.text.strip(), flags=re.IGNORECASE)
if match: if match:
self.dead = True self.dead = True
return self.callback(slack, msg, match) return self.callback(event, match)
else: else:
return None return None
@ -263,3 +427,44 @@ class Passive(object):
async def run(self) -> None: async def run(self) -> None:
# Run this passive routed through the specified slack client. # Run this passive routed through the specified slack client.
raise NotImplementedError() raise NotImplementedError()
"""
Methods for easily responding to messages, etc.
"""
T = 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, event: Event):
self.event = event
async def __call__(self, awt: Awaitable[T]) -> T:
try:
return await awt
except Exception as e:
get_slack().reply(self.event, "Error: {}".format(str(e)), True)
raise e
"""
Miscellania
"""
A, B, C = TypeVar("A"), TypeVar("B"), TypeVar("C")
# Prints exceptions instead of silently dropping them in async tasks
async def _exception_printing_task(c: Coroutine[A, B, C]) -> Coroutine[A, B, C]:
# Print exceptions as they pass through
try:
return await c
except Exception:
traceback.print_exc()
raise

View File

@ -4,7 +4,6 @@ from typing import Match
from slackclient import SlackClient from slackclient import SlackClient
import channel_util
import house_management import house_management
import identifier import identifier
import slack_util import slack_util
@ -19,16 +18,16 @@ def fmt_work_dict(work_dict: dict) -> str:
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
async def count_work_callback(slack: SlackClient, msg: dict, match: Match) -> None: async def count_work_callback(event: slack_util.Event, match: Match) -> None:
# Make an error wrapper # Make an error wrapper
verb = slack_util.VerboseWrapper(slack, msg) verb = slack_util.VerboseWrapper(event)
# Tidy the text # Tidy the text
text = msg["text"].lower().strip() text = event.message.text.strip()
# Couple things to work through. # Couple things to work through.
# One: Who sent the message? # One: Who sent the message?
who_wrote = await verb(identifier.lookup_msg_brother(msg)) who_wrote = await verb(event.user.as_user().get_brother())
who_wrote_label = "{} [{}]".format(who_wrote.name, who_wrote.scroll) who_wrote_label = "{} [{}]".format(who_wrote.name, who_wrote.scroll)
# Two: What work did they do? # Two: What work did they do?
@ -42,7 +41,7 @@ async def count_work_callback(slack: SlackClient, msg: dict, match: Match) -> No
# Three: check if we found anything # Three: check if we found anything
if len(new_work) == 0: if len(new_work) == 0:
if re.search(r'\s\d\s', text) is not None: if re.search(r'\s\d\s', text) is not None:
slack_util.reply(slack, msg, slack_util.get_slack().reply(event,
"If you were trying to record work, it was not recognized.\n" "If you were trying to record work, it was not recognized.\n"
"Use words {} or work will not be recorded".format(counted_data)) "Use words {} or work will not be recorded".format(counted_data))
return return
@ -59,7 +58,7 @@ async def count_work_callback(slack: SlackClient, msg: dict, match: Match) -> No
fmt_work_dict(new_work), fmt_work_dict(new_work),
contribution_count, contribution_count,
new_total)) new_total))
slack_util.reply(slack, msg, congrats) slack_util.get_slack().reply(event, congrats)
async def record_towel_contribution(for_brother: Brother, contribution_count: int) -> int: async def record_towel_contribution(for_brother: Brother, contribution_count: int) -> int:
@ -91,5 +90,5 @@ async def record_towel_contribution(for_brother: Brother, contribution_count: in
# Make dem HOOKs # Make dem HOOKs
count_work_hook = slack_util.ChannelHook(count_work_callback, count_work_hook = slack_util.ChannelHook(count_work_callback,
patterns=".*", patterns=".*",
channel_whitelist=[channel_util.SLAVES_TO_THE_MACHINE_ID], channel_whitelist=["#slavestothemachine"],
consumer=False) consumer=False)