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