Semi-stable rework of signoff procedure
This commit is contained in:
parent
5ae139fde0
commit
716ec52a98
|
|
@ -0,0 +1,115 @@
|
|||
import asyncio
|
||||
from typing import List, Any, AsyncGenerator
|
||||
|
||||
from slackclient import SlackClient # Obvious
|
||||
|
||||
import channel_util
|
||||
import slack_util
|
||||
from dummy import FakeClient
|
||||
|
||||
# Read the API token
|
||||
api_file = open("apitoken.txt", 'r')
|
||||
SLACK_API = next(api_file).strip()
|
||||
api_file.close()
|
||||
|
||||
# Enable to use dummy
|
||||
DEBUG_MODE = False
|
||||
|
||||
|
||||
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
|
||||
if DEBUG_MODE:
|
||||
self.slack = FakeClient()
|
||||
else:
|
||||
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 _ in self.spool_tasks():
|
||||
print("Handling a message...!")
|
||||
|
||||
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']))
|
||||
|
||||
# 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(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")
|
||||
|
||||
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 get_client_wrapper() -> ClientWrapper:
|
||||
return _singleton
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
import dataclasses
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, timedelta
|
||||
from typing import Tuple, List, Optional, Any
|
||||
|
||||
import google_api
|
||||
import scroll_util
|
||||
|
||||
SHEET_ID = "1lPj9GjB00BuIq9GelOWh5GmiGsheLlowPnHLnWBvMOM"
|
||||
|
||||
# Note: These ranges use named range feature of google sheets.
|
||||
# To edit range of jobs, edit the named range in Data -> Named Ranges
|
||||
job_range = "AllJobs" # Note that the first row is headers
|
||||
point_range = "PointRange"
|
||||
|
||||
# How tolerant of spelling errors in names to be
|
||||
SHEET_LOOKUP_THRESHOLD = 0.9
|
||||
|
||||
JOB_VAL = 1
|
||||
LATE_VAL = 0.5
|
||||
MISS_VAL = -1
|
||||
SIGNOFF_VAL = 0.1
|
||||
|
||||
# What to put for a non-signed-off job
|
||||
SIGNOFF_PLACEHOLDER = "E-SIGNOFF"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Job(object):
|
||||
"""
|
||||
Represents a job in a more internally meaningful way.
|
||||
"""
|
||||
name: str
|
||||
house: str
|
||||
day_of_week: str
|
||||
# Extra stuff, interpreted
|
||||
day: Optional[date]
|
||||
|
||||
|
||||
@dataclass
|
||||
class JobAssignment(object):
|
||||
"""
|
||||
Tracks a job's assignment and completion
|
||||
"""
|
||||
job: Job
|
||||
assignee: scroll_util.Brother
|
||||
signer: Optional[scroll_util.Brother]
|
||||
late: bool
|
||||
|
||||
def to_raw(self) -> Tuple[str, str, str, str, str, str]:
|
||||
# Converts this back into a spreadsheet row
|
||||
signer_name = self.signer.name if self.signer is not None else SIGNOFF_PLACEHOLDER
|
||||
late = "y" if self.late else "n"
|
||||
return self.job.name, self.job.house, self.job.day_of_week, self.assignee.name, signer_name, late
|
||||
|
||||
|
||||
@dataclass
|
||||
class PointStatus(object):
|
||||
"""
|
||||
Tracks a brothers points
|
||||
"""
|
||||
brother_raw: str
|
||||
brother: scroll_util.Brother # Interpereted
|
||||
job_points: float = 0
|
||||
signoff_points: float = 0
|
||||
towel_points: float = 0
|
||||
work_party_points: float = 0
|
||||
bonus_points: float = 0
|
||||
|
||||
def to_raw(self) -> Tuple[str, str, str, str, str, str]:
|
||||
# Convert to a row. Also, do some rounding while we're at it
|
||||
fstring = "{:.2f}"
|
||||
return (self.brother_raw,
|
||||
fstring.format(self.job_points),
|
||||
fstring.format(self.signoff_points),
|
||||
fstring.format(self.towel_points),
|
||||
fstring.format(self.work_party_points),
|
||||
fstring.format(self.bonus_points),
|
||||
)
|
||||
|
||||
|
||||
def strip_all(l: List[str]) -> List[str]:
|
||||
return [x.strip() for x in l]
|
||||
|
||||
|
||||
def import_assignments() -> List[Optional[JobAssignment]]:
|
||||
"""
|
||||
Imports Jobs and JobAssignments from the sheet. 1:1 row correspondence.
|
||||
"""
|
||||
# Get the raw data
|
||||
job_rows = google_api.get_sheet_range(SHEET_ID, job_range)
|
||||
|
||||
# None-out invalid rows (length not at least 4, which includes the 4 most important features)
|
||||
def fixer(row):
|
||||
if len(row) == 4:
|
||||
return strip_all(row + [SIGNOFF_PLACEHOLDER, "n"])
|
||||
elif len(row) == 5:
|
||||
return strip_all(row + "n")
|
||||
elif len(row) == 6:
|
||||
return strip_all(row)
|
||||
else:
|
||||
return None
|
||||
|
||||
# Apply the fix
|
||||
job_rows = [fixer(row) for row in job_rows]
|
||||
|
||||
# Now, create jobs
|
||||
assignments = []
|
||||
for row in job_rows:
|
||||
if row is None:
|
||||
assignments.append(None)
|
||||
else:
|
||||
# Breakout list
|
||||
job_name, location, day, assignee, signer, late = row
|
||||
|
||||
# Figure out when the day actually is, in terms of the date class
|
||||
day_rank = {
|
||||
"monday": 0,
|
||||
"tuesday": 1,
|
||||
"wednesday": 2,
|
||||
"thursday": 3,
|
||||
"friday": 4,
|
||||
"saturday": 5,
|
||||
"sunday": 6
|
||||
}.get(day.lower(), None)
|
||||
|
||||
if day_rank is not None:
|
||||
# Figure out current date day of week, and extrapolate the jobs day of week from there
|
||||
today = date.today()
|
||||
today_rank = today.weekday()
|
||||
|
||||
days_till = day_rank - today_rank
|
||||
if days_till <= 0:
|
||||
days_till += 7
|
||||
|
||||
# Now we know what day it is!
|
||||
job_day = today + timedelta(days=days_till)
|
||||
else:
|
||||
# Can't win 'em all
|
||||
job_day = None
|
||||
|
||||
# Create the job
|
||||
job = Job(name=job_name, house=location, day_of_week=day, day=job_day)
|
||||
|
||||
# Now make an assignment for the job
|
||||
# Find the brother it is assigned to
|
||||
try:
|
||||
assignee = scroll_util.find_by_name(assignee, SHEET_LOOKUP_THRESHOLD)
|
||||
except scroll_util.BadName:
|
||||
# If we can't get one close enough, make a dummy
|
||||
assignee = scroll_util.Brother(assignee, scroll_util.MISSINGBRO_SCROLL)
|
||||
|
||||
# Find the brother who is currently listed as having signed it off
|
||||
try:
|
||||
if signer == SIGNOFF_PLACEHOLDER:
|
||||
signer = None
|
||||
else:
|
||||
signer = scroll_util.find_by_name(signer)
|
||||
except scroll_util.BadName:
|
||||
# If we can't figure out the name
|
||||
signer = None
|
||||
|
||||
# Make late a bool
|
||||
late = late == "y"
|
||||
|
||||
# Create the assignment
|
||||
assignment = JobAssignment(job=job, assignee=assignee, signer=signer, late=late)
|
||||
|
||||
# Append to job/assignment lists
|
||||
assignments.append(assignment)
|
||||
|
||||
# Git 'em gone
|
||||
return assignments
|
||||
|
||||
|
||||
def export_assignments(assigns: List[Optional[JobAssignment]]) -> None:
|
||||
# Smash to rows
|
||||
rows = []
|
||||
for v in assigns:
|
||||
if v is None:
|
||||
rows.append([""] * 6)
|
||||
else:
|
||||
rows.append(list(v.to_raw()))
|
||||
|
||||
# Send to google
|
||||
google_api.set_sheet_range(SHEET_ID, job_range, rows)
|
||||
|
||||
|
||||
def import_points() -> (List[str], List[PointStatus]):
|
||||
# Figure out how many things there are in a point status
|
||||
field_count = len(dataclasses.fields(PointStatus))
|
||||
|
||||
# Get the raw data
|
||||
point_rows = google_api.get_sheet_range(SHEET_ID, point_range)
|
||||
|
||||
# Get the headers
|
||||
headers = point_rows[0]
|
||||
point_rows = point_rows[1:]
|
||||
|
||||
# Tidy rows up
|
||||
def converter(row: List[Any]) -> Optional[PointStatus]:
|
||||
# If its too long, or empty already, ignore
|
||||
if len(row) == 0 or len(row) > field_count:
|
||||
return None
|
||||
|
||||
# Ensure its the proper length
|
||||
row: List[Any] = row + ([0] * (field_count - len(row) - 1))
|
||||
|
||||
# Ensure all past the first column are float. If can't convert, make 0
|
||||
for i in range(1, len(row)):
|
||||
try:
|
||||
x = float(row[i])
|
||||
except ValueError:
|
||||
x = 0
|
||||
row[i] = x
|
||||
|
||||
# Get the brother for the last item
|
||||
real_brother = scroll_util.find_by_name(row[0], recent_only=True)
|
||||
|
||||
# Ok! Now, we just map it directly to a PointStatus
|
||||
status = PointStatus(row[0], real_brother, *(row[1:]))
|
||||
return status
|
||||
|
||||
# Perform conversion and return
|
||||
point_statuses = [converter(row) for row in point_rows]
|
||||
return headers, point_statuses
|
||||
|
||||
|
||||
def export_points(headers: List[str], points: List[PointStatus]) -> None:
|
||||
# Smash to rows
|
||||
rows = [list(point_status.to_raw()) for point_status in points]
|
||||
rows = [headers] + rows
|
||||
|
||||
# Send to google
|
||||
google_api.set_sheet_range(SHEET_ID, point_range, rows)
|
||||
|
||||
|
||||
def apply_house_points(points: List[PointStatus], assigns: List[Optional[JobAssignment]]):
|
||||
"""
|
||||
Modifies the points list to reflect job assignment scores.
|
||||
Destroys existing values in the column
|
||||
"""
|
||||
# First, eliminate all house points
|
||||
for p in points:
|
||||
p.job_points = 0
|
||||
|
||||
# Then, apply each assign
|
||||
for a in assigns:
|
||||
# Ignore null assigns
|
||||
if a is None:
|
||||
continue
|
||||
|
||||
# What modifier should this have?
|
||||
if a.signer is None:
|
||||
score = MISS_VAL
|
||||
else:
|
||||
if a.late:
|
||||
score = LATE_VAL
|
||||
else:
|
||||
score = JOB_VAL
|
||||
|
||||
# Find the corr bro in points
|
||||
for p in points:
|
||||
# If we find, add the score and stop looking
|
||||
if p.brother == a.assignee:
|
||||
p.job_points += score
|
||||
break
|
||||
|
||||
if p.brother == a.signer:
|
||||
p.signoff_points += SIGNOFF_VAL
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
from typing import List, Match, Callable, TypeVar, Optional, Iterable
|
||||
|
||||
from fuzzywuzzy import fuzz
|
||||
from slackclient import SlackClient
|
||||
|
||||
import channel_util
|
||||
import house_management
|
||||
import identifier
|
||||
import scroll_util
|
||||
import slack_util
|
||||
|
||||
SHEET_ID = "1lPj9GjB00BuIq9GelOWh5GmiGsheLlowPnHLnWBvMOM"
|
||||
|
||||
MIN_RATIO = 0.9
|
||||
|
||||
|
||||
def alert_user(slack: SlackClient, brother: scroll_util.Brother, saywhat: str) -> None:
|
||||
"""
|
||||
DM a brother saying something
|
||||
"""
|
||||
# We do this as a for loop just in case multiple people reg. to same scroll for some reason (e.g. dup accounts)
|
||||
for slack_id in identifier.lookup_brother_userids(brother):
|
||||
dm_id = slack_util.im_channel_for_id(slack, slack_id)
|
||||
if dm_id:
|
||||
# Give a dummy msg dict, since we won't actually be using anything in it
|
||||
slack_util.send_message(slack, saywhat, dm_id)
|
||||
else:
|
||||
print("Warning: unable to find dm for brother {}".format(brother))
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def tiemax(items: Iterable[T], key: Callable[[T], float], min_score: Optional[float] = None) -> List[T]:
|
||||
best = []
|
||||
best_score = min_score
|
||||
for elt in items:
|
||||
score = key(elt)
|
||||
if best_score is None or score > best_score:
|
||||
best_score = score
|
||||
best = [elt]
|
||||
elif score == best_score:
|
||||
best.append(elt)
|
||||
return best
|
||||
|
||||
|
||||
def do_signoff(slack: SlackClient, msg: dict, on_assign_index: int, by_brother: scroll_util.Brother) -> None:
|
||||
# First things first: Get the signoffs
|
||||
assignments = house_management.import_assignments()
|
||||
|
||||
# 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.house,
|
||||
on_assign.job.day_of_week,
|
||||
on_assign.job.name))
|
||||
alert_user(slack, on_assign.assignee, "Your house job was signed off")
|
||||
|
||||
|
||||
async def signoff_callback(slack: SlackClient, msg: dict, match: Match) -> None:
|
||||
"""
|
||||
Callback to signoff a user.
|
||||
"""
|
||||
# Find out who this is
|
||||
signee_name = match.group(1)
|
||||
|
||||
# Fix with a quick lookup
|
||||
try:
|
||||
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
|
||||
assigns = house_management.import_assignments()
|
||||
|
||||
# Find closest assignment to what we're after
|
||||
def scorer(a: Optional[house_management.JobAssignment]) -> float:
|
||||
if a is None:
|
||||
return 0
|
||||
else:
|
||||
return fuzz.ratio(signee.name, a.assignee.name)
|
||||
|
||||
closest_assigns = tiemax(assigns, key=scorer)
|
||||
|
||||
# Remove those that are already signed off
|
||||
closest_assigns = [c for c in closest_assigns if c is not None]
|
||||
closest_assigns = [c for c in closest_assigns if c.signer is None]
|
||||
|
||||
# If there aren't any jobs, say so
|
||||
if len(closest_assigns) == 0:
|
||||
slack_util.reply(slack, msg, "Unable to find any jobs assigned to brother {} "
|
||||
"(identified as {}).".format(signee_name, signee.name))
|
||||
return
|
||||
|
||||
# If theres only one job, sign it off
|
||||
elif len(closest_assigns) == 1:
|
||||
targ_assign = closest_assigns[0]
|
||||
|
||||
# Where is it?
|
||||
targ_assign_index = assigns.index(targ_assign)
|
||||
|
||||
do_signoff(slack, msg, targ_assign_index, signer)
|
||||
return
|
||||
|
||||
# If theres multiple jobs, we need to get a follow up!
|
||||
else:
|
||||
slack_util.reply(slack, msg, "Dunno how to handle multiple jobs yet")
|
||||
return
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
async def reset_callback(slack: SlackClient, msg: dict, match: Match) -> None:
|
||||
"""
|
||||
Resets the scores.
|
||||
"""
|
||||
# Get curr rows
|
||||
headers, points = house_management.import_points()
|
||||
|
||||
# Set to 0/default
|
||||
for i in range(len(points)):
|
||||
new = house_management.PointStatus(brother_raw=points[i].brother_raw, brother=points[i].brother)
|
||||
points[i] = new
|
||||
|
||||
house_management.export_points(headers, points)
|
||||
|
||||
# Now unsign everything
|
||||
assigns = house_management.import_assignments()
|
||||
for a in assigns:
|
||||
if a is not None:
|
||||
a.signer = None
|
||||
house_management.export_assignments(assigns)
|
||||
|
||||
slack_util.reply(slack, msg, "Reset scores and signoffs")
|
||||
|
||||
|
||||
async def nag_callback(slack, msg, match):
|
||||
# Get the day
|
||||
day = match.group(1).lower().strip()
|
||||
|
||||
# Get the assigns
|
||||
assigns = house_management.import_assignments()
|
||||
|
||||
# Filter to day
|
||||
assigns = [assign for assign in assigns if assign.job.day_of_week.lower() == day]
|
||||
|
||||
# Filter signed off
|
||||
assigns = [assign for assign in assigns if assign.signer is None]
|
||||
|
||||
# If no jobs found, somethings up. Probably mispelled day.
|
||||
if not assigns:
|
||||
slack_util.reply(slack, msg, "No jobs found. Check that the day is spelled correctly, with no extra symbols.\n"
|
||||
"It is possible that all jobs have been signed off, as well.",
|
||||
in_thread=True)
|
||||
return
|
||||
|
||||
# Nag each
|
||||
response = "Do yer jerbs! They are as follows:\n"
|
||||
for assign in assigns:
|
||||
# Make the row template
|
||||
response += "({}) {} -- {} ".format(assign.job.house, assign.job.name, assign.assignee.name)
|
||||
|
||||
# Find the people to @
|
||||
brother_slack_ids = 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"
|
||||
|
||||
slack_util.reply(slack, msg, response, in_thread=False, to_channel=channel_util.BOTZONE)
|
||||
|
||||
|
||||
signoff_hook = slack_util.Hook(signoff_callback,
|
||||
pattern=r"testsignoff\s+(.*)",
|
||||
channel_whitelist=[channel_util.HOUSEJOBS])
|
||||
|
||||
reset_hook = slack_util.Hook(reset_callback,
|
||||
pattern=r"testreset signoffs",
|
||||
channel_whitelist=[channel_util.COMMAND_CENTER_ID]) # COMMAND_CENTER_ID
|
||||
|
||||
nag_hook = slack_util.Hook(nag_callback,
|
||||
pattern=r"nagjobs\s*(.*)",
|
||||
channel_whitelist=[channel_util.COMMAND_CENTER_ID])
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import identifier
|
||||
import scroll_util
|
||||
import slack_util
|
||||
import google_api
|
||||
import channel_util
|
||||
|
||||
SHEET_ID = "1lPj9GjB00BuIq9GelOWh5GmiGsheLlowPnHLnWBvMOM"
|
||||
|
||||
# Note: These ranges use named range feature of google sheets.
|
||||
# To edit range of jobs, edit the named range in Data -> Named Ranges
|
||||
eight_job_range = "EightJobs" # Format: Job Day Bro
|
||||
fiftythree_job_range = "FiftyThreeJobs"
|
||||
|
||||
|
||||
class Job(object):
|
||||
"""
|
||||
Object representing a house job
|
||||
"""
|
||||
def __init__(self, house: str, job_name: str, day: str, brother_name: str):
|
||||
self.house, self.job_name, self.day, self.brother_name = house, job_name, day, brother_name
|
||||
self.day = self.day.lower().strip()
|
||||
|
||||
|
||||
def get_jobs(day=None):
|
||||
"""
|
||||
Retrieves the house jobs for a given day.
|
||||
If no day is provided, returns all house jobs for the week.
|
||||
|
||||
:param day: Day to compare the day column to. If equal, keep. If Not, discard. If day==None, ignore this filter
|
||||
:return: list of Job objects
|
||||
"""
|
||||
|
||||
# Get the spreadsheet section
|
||||
eight_jobs = google_api.get_sheet_range(SHEET_ID, eight_job_range)
|
||||
ft_jobs = google_api.get_sheet_range(SHEET_ID, fiftythree_job_range)
|
||||
|
||||
# Turn to job objects
|
||||
def valid_row(x):
|
||||
try:
|
||||
return len(x) == 3
|
||||
except (AttributeError, TypeError):
|
||||
return False
|
||||
|
||||
eight_jobs = [Job("8", *r) for r in eight_jobs if valid_row(r)]
|
||||
ft_jobs = [Job("53", *r) for r in ft_jobs if valid_row(r)]
|
||||
jobs = eight_jobs + ft_jobs
|
||||
|
||||
# Filter to day
|
||||
if day:
|
||||
jobs = [j for j in jobs if j.day == day]
|
||||
|
||||
return jobs
|
||||
|
||||
|
||||
async def nag_callback(slack, msg, match):
|
||||
# Get the day
|
||||
day = match.group(1).lower().strip()
|
||||
jobs = get_jobs(day)
|
||||
|
||||
# If no jobs found, somethings up. Probably mispelled day.
|
||||
if not jobs:
|
||||
slack_util.reply(slack, msg, "No jobs found. Check that the day is spelled correctly, with no extra symbols",
|
||||
in_thread=True)
|
||||
return
|
||||
|
||||
# Nag each
|
||||
response = "Do yer jerbs! They are as follows:\n"
|
||||
for job in jobs:
|
||||
# Make the row template
|
||||
response += "({}) {} -- ".format(job.house, job.job_name)
|
||||
|
||||
# Find the people to @
|
||||
brother = scroll_util.find_by_name(job.brother_name)
|
||||
brother_slack_ids = identifier.lookup_brother_userids(brother)
|
||||
|
||||
if brother_slack_ids:
|
||||
for slack_id in brother_slack_ids:
|
||||
response += "<@{}> ".format(slack_id)
|
||||
else:
|
||||
response += "{} (scroll missing. Please register for @ pings!)".format(job.brother_name)
|
||||
response += "\n"
|
||||
|
||||
slack_util.reply(slack, msg, response, in_thread=False, to_channel=channel_util.GENERAL)
|
||||
|
||||
|
||||
nag_hook = slack_util.Hook(nag_callback, pattern=r"nagjobs\s*(.*)", channel_whitelist=[channel_util.COMMAND_CENTER_ID])
|
||||
199
job_signoff.py
199
job_signoff.py
|
|
@ -1,199 +0,0 @@
|
|||
from typing import List, Any, Match, Tuple
|
||||
|
||||
from fuzzywuzzy import process
|
||||
from slackclient import SlackClient
|
||||
|
||||
import channel_util
|
||||
import google_api
|
||||
import identifier
|
||||
import job_nagger
|
||||
import slack_util
|
||||
import scroll_util
|
||||
|
||||
SHEET_ID = "1lPj9GjB00BuIq9GelOWh5GmiGsheLlowPnHLnWBvMOM"
|
||||
|
||||
# Note: These ranges use named range feature of google sheets.
|
||||
# To edit range of jobs, edit the named range in Data -> Named Ranges in the Goole Sheets page
|
||||
output_range = "JobScoreTrackerV2"
|
||||
|
||||
MIN_RATIO = 0.9
|
||||
SIGNOFF_REWARD = 0.1
|
||||
|
||||
|
||||
def get_curr_points() -> List[Tuple[str, float, str]]:
|
||||
"""
|
||||
:return: The current contents of the output range
|
||||
"""
|
||||
# Get the current stuff
|
||||
curr_output = google_api.get_sheet_range(SHEET_ID, output_range)
|
||||
all_jobs = job_nagger.get_jobs()
|
||||
|
||||
# Each element: force length of 3. Fmt name, score, job
|
||||
def row_fixer(r: List[Any]) -> Tuple[str, float, str]:
|
||||
if len(r) == 0:
|
||||
return "", 0, "No Job"
|
||||
else:
|
||||
# Get the appropriate score
|
||||
if len(r) > 1:
|
||||
try:
|
||||
curr_score = float(r[1])
|
||||
except ValueError:
|
||||
curr_score = 0
|
||||
else:
|
||||
curr_score = 0
|
||||
|
||||
# Find the current job
|
||||
job = "No Job"
|
||||
for j in all_jobs:
|
||||
if j.brother_name == r[0]:
|
||||
job = "{} ({} - {})".format(j.job_name, j.day, j.house)
|
||||
break
|
||||
return r[0], curr_score, job
|
||||
|
||||
# Fix each row
|
||||
curr_output = [row_fixer(x) for x in curr_output]
|
||||
|
||||
# Cut off the chaff - doesn't seem to be necessary.
|
||||
return curr_output
|
||||
|
||||
|
||||
def put_points(vals: List[Tuple[str, float, str]]) -> None:
|
||||
# Turn the tuples to lists
|
||||
vals = [list(row) for row in vals]
|
||||
google_api.set_sheet_range(SHEET_ID, output_range, vals)
|
||||
|
||||
|
||||
def alert_user(slack: SlackClient, name: str, saywhat: str) -> None:
|
||||
"""
|
||||
DM a brother saying something
|
||||
"""
|
||||
brother_dict = scroll_util.find_by_name(name)
|
||||
# We do this as a for loop just in case multiple people reg. to same scroll for some reason (e.g. dup accounts)
|
||||
for slack_id in identifier.lookup_brother_userids(brother_dict):
|
||||
dm_id = slack_util.im_channel_for_id(slack, slack_id)
|
||||
if dm_id:
|
||||
# Give a dummy msg dict, since we won't actually be using anything in it
|
||||
slack_util.reply(slack, {}, saywhat, to_channel=dm_id, in_thread=False)
|
||||
else:
|
||||
print("Warning: unable to find dm for brother {}".format(brother_dict))
|
||||
|
||||
|
||||
async def signoff_callback(slack: SlackClient, msg: dict, match: Match) -> None:
|
||||
"""
|
||||
Callback to signoff a user.
|
||||
"""
|
||||
# Find the index of our person.
|
||||
name = match.group(1)
|
||||
|
||||
# Also, who just signed us off?
|
||||
signer = identifier.lookup_msg_brother(msg).name
|
||||
|
||||
# Try giving the person a point
|
||||
try:
|
||||
(bro_name, bro_total, bro_job), (ass_name, ass_total, _) = adjust_scores((name, 1), (signer, SIGNOFF_REWARD))
|
||||
slack_util.reply(slack, msg, "Gave {} one housejob point for job {}.\n"
|
||||
"They now have {} for this period.\n"
|
||||
"You ({}) were credited with the signoff".format(bro_name, bro_job, bro_total, ass_name))
|
||||
alert_user(slack, bro_name,
|
||||
"You, who we believe to be {}, just had your house job signed off by {}!".format(bro_name, ass_name))
|
||||
except scroll_util.BadName as e:
|
||||
# We didn't find a name - no action was performed.
|
||||
slack_util.reply(slack, msg, e.as_response())
|
||||
|
||||
|
||||
async def punish_callback(slack: SlackClient, msg: dict, match: Match) -> None:
|
||||
"""
|
||||
Undoes a signoff. Maybe should rename
|
||||
"""
|
||||
# Find the index of our person.
|
||||
name = match.group(2)
|
||||
|
||||
# Also, who just signed us off?
|
||||
signer = identifier.lookup_msg_brother(msg).name
|
||||
|
||||
# Try giving the person a point
|
||||
try:
|
||||
(bro_name, bro_total, _), (ass_name, ass_total, _) = adjust_scores((name, -1), (signer, -SIGNOFF_REWARD))
|
||||
slack_util.reply(slack, msg, "Took one housejob point from {}.\n"
|
||||
"They now have {} for this period.\n"
|
||||
"Under the assumption that this was to undo a mistake, we have deducted the "
|
||||
"usual signoff reward from you, ({}).\n "
|
||||
"You can easily earn it back by signing off the right person ;).".format(bro_name,
|
||||
bro_total,
|
||||
ass_name))
|
||||
alert_user(slack, bro_name,
|
||||
"You, who we believe to be {}, just had your house job UN-signed off by {}.\n"
|
||||
"Perhaps the asshoman made a mistake when they first signed you off.\n"
|
||||
"If you believe that they undid the signoff accidentally, go talk to them".format(bro_name, signer))
|
||||
|
||||
except scroll_util.BadName as e:
|
||||
# We didn't find a name - no action was performed.
|
||||
slack_util.reply(slack, msg, e.as_response())
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
async def reset_callback(slack: SlackClient, msg: dict, match: Match) -> None:
|
||||
"""
|
||||
Resets the scores.
|
||||
"""
|
||||
# Get curr rows
|
||||
points = get_curr_points()
|
||||
|
||||
new_points = [(a, 0, c) for a, _, c in points]
|
||||
put_points(new_points)
|
||||
slack_util.reply(slack, msg, "Reset scores")
|
||||
|
||||
|
||||
def adjust_scores(*name_delta_tuples: Tuple[str, float]) -> List[Tuple[str, float, str]]:
|
||||
"""
|
||||
Helper that uses a sequence of tuples in the format (name, delta) to adjust each (name) to have +delta score.
|
||||
Operation performed as a batch.
|
||||
:param name_delta_tuples: The name + score deltas
|
||||
:return: The updated tuples rows.
|
||||
"""
|
||||
# Get the current stuff
|
||||
points = get_curr_points()
|
||||
names = [p[0] for p in points]
|
||||
modified_user_indexes = []
|
||||
|
||||
for name, delta in name_delta_tuples:
|
||||
# Find our guy
|
||||
target_name, ratio = process.extractOne(name, names)
|
||||
ratio = ratio / 100.0
|
||||
|
||||
# If bad ratio, error
|
||||
if ratio < MIN_RATIO:
|
||||
raise scroll_util.BadName(target_name, ratio, MIN_RATIO)
|
||||
|
||||
# Where is he in the list?
|
||||
target_index = names.index(target_name)
|
||||
|
||||
# Get his current score
|
||||
curr_score = points[target_index][1]
|
||||
curr_job = points[target_index][2]
|
||||
|
||||
# target should be in the form index, (name, score)
|
||||
target_new = target_name, curr_score + delta, curr_job
|
||||
|
||||
# Put it back
|
||||
points[target_index] = target_new
|
||||
|
||||
# Record where we edited
|
||||
modified_user_indexes.append(target_index)
|
||||
|
||||
# Push all to sheets if exit loop without error
|
||||
put_points(points)
|
||||
|
||||
# Conver indexes to rows, then return the adjusted name/score_tuples
|
||||
return [points[i] for i in modified_user_indexes]
|
||||
|
||||
|
||||
signoff_hook = slack_util.Hook(signoff_callback,
|
||||
pattern=r"signoff\s+(.*)",
|
||||
channel_whitelist=[channel_util.HOUSEJOBS])
|
||||
undosignoff_hook = slack_util.Hook(punish_callback,
|
||||
pattern=r"(unsignoff|undosignoff|undo)\s+(.*)",
|
||||
channel_whitelist=[channel_util.HOUSEJOBS])
|
||||
reset_hook = slack_util.Hook(reset_callback,
|
||||
pattern=r"reset signoffs",
|
||||
channel_whitelist=[channel_util.COMMAND_CENTER_ID])
|
||||
130
main.py
130
main.py
|
|
@ -1,26 +1,15 @@
|
|||
import asyncio
|
||||
from typing import List, Any, AsyncGenerator
|
||||
|
||||
from slackclient import SlackClient # Obvious
|
||||
|
||||
import channel_util
|
||||
import identifier
|
||||
import job_nagger
|
||||
import job_signoff
|
||||
import job_commands
|
||||
import management_commands
|
||||
import periodicals
|
||||
import scroll_util
|
||||
import slack_util
|
||||
import slavestothemachine
|
||||
from dummy import FakeClient
|
||||
|
||||
# Read api token from file
|
||||
api_file = open("apitoken.txt", 'r')
|
||||
SLACK_API = next(api_file).strip()
|
||||
api_file.close()
|
||||
|
||||
# Enable to use dummy
|
||||
DEBUG_MODE = False
|
||||
from client_wrapper import ClientWrapper
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
|
@ -39,7 +28,7 @@ def main() -> None:
|
|||
wrap.add_hook(channel_util.channel_check_hook)
|
||||
|
||||
# Add nagging functionality
|
||||
wrap.add_hook(job_nagger.nag_hook)
|
||||
wrap.add_hook(job_commands.nag_hook)
|
||||
|
||||
# Add kill switch
|
||||
wrap.add_hook(management_commands.reboot_hook)
|
||||
|
|
@ -49,127 +38,24 @@ def main() -> None:
|
|||
wrap.add_hook(slavestothemachine.dump_work_hook)
|
||||
|
||||
# Add signoffs
|
||||
wrap.add_hook(job_signoff.signoff_hook)
|
||||
wrap.add_hook(job_signoff.undosignoff_hook)
|
||||
wrap.add_hook(job_signoff.reset_hook)
|
||||
wrap.add_hook(job_commands.signoff_hook)
|
||||
wrap.add_hook(job_commands.reset_hook)
|
||||
|
||||
# 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))
|
||||
# help_callback = management_commands.list_hooks_callback_gen(wrap.hooks)
|
||||
# wrap.add_hook(slack_util.Hook(help_callback, pattern=management_commands.bot_help_pattern))
|
||||
|
||||
# Add boozebot
|
||||
wrap.add_passive(periodicals.ItsTenPM())
|
||||
|
||||
event_loop = asyncio.get_event_loop()
|
||||
event_loop.set_debug(True)
|
||||
message_handling = wrap.respond_messages()
|
||||
passive_handling = wrap.run_passives()
|
||||
both = asyncio.gather(message_handling, passive_handling)
|
||||
event_loop.run_until_complete(both)
|
||||
|
||||
|
||||
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
|
||||
if DEBUG_MODE:
|
||||
self.slack = FakeClient()
|
||||
else:
|
||||
self.slack = SlackClient(SLACK_API)
|
||||
|
||||
# For overriding output channel
|
||||
self.debug_slack = slack_util.SlackDebugCondom(self.slack)
|
||||
|
||||
# Hooks go regex -> callback on (slack, msg, match)
|
||||
self.hooks: List[slack_util.Hook] = []
|
||||
|
||||
# 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.Hook) -> None:
|
||||
self.hooks.append(hook)
|
||||
|
||||
async def respond_messages(self) -> None:
|
||||
"""
|
||||
Asynchronous tasks that eternally reads and responds to messages.
|
||||
"""
|
||||
async for _ in self.spool_tasks():
|
||||
print("Handling a message...!")
|
||||
|
||||
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']))
|
||||
|
||||
# Handle debug
|
||||
if msg['text'][:6] == "DEBUG ":
|
||||
slack_to_use = self.debug_slack
|
||||
msg['text'] = msg['text'][6:]
|
||||
print("Debug handling \"{}\"".format(msg['text']))
|
||||
else:
|
||||
slack_to_use = self.slack
|
||||
|
||||
# Msg is good
|
||||
# Find which hook, if any, satisfies
|
||||
sat_hook = None
|
||||
sat_match = None
|
||||
for hook in self.hooks:
|
||||
match = hook.check(msg)
|
||||
if match is not None:
|
||||
sat_match = match
|
||||
sat_hook = hook
|
||||
break
|
||||
|
||||
# If no hooks, continue
|
||||
if not sat_hook:
|
||||
continue
|
||||
|
||||
# Throw up as a task, otherwise
|
||||
coro = sat_hook.invoke(slack_to_use, msg, sat_match)
|
||||
task = asyncio.create_task(coro)
|
||||
yield task
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# run main
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class ItsTenPM(slack_util.Passive):
|
|||
await asyncio.sleep(delay)
|
||||
|
||||
# Crow like a rooster
|
||||
slack_util.reply(slack, {}, "IT'S 10 PM!", in_thread=False, to_channel=channel_util.RANDOM)
|
||||
slack_util.send_message(slack, "IT'S 10 PM!", channel_util.RANDOM)
|
||||
|
||||
# Wait a while before trying it again, to prevent duplicates
|
||||
await asyncio.sleep(60)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ Only really kept separate for neatness sake.
|
|||
"""
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Match
|
||||
|
||||
from fuzzywuzzy import process
|
||||
|
|
@ -11,17 +12,17 @@ from slackclient import SlackClient
|
|||
|
||||
import slack_util
|
||||
|
||||
# Use this if we can't figure out who a brother actually is
|
||||
MISSINGBRO_SCROLL = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class Brother(object):
|
||||
"""
|
||||
Represents a brother.
|
||||
"""
|
||||
def __init__(self, name: str, scroll: int):
|
||||
self.name = name
|
||||
self.scroll = scroll
|
||||
|
||||
def __repr__(self):
|
||||
return "<Brother {}-{}>".format(self.name, self.scroll)
|
||||
name: str
|
||||
scroll: int
|
||||
|
||||
|
||||
# load the family tree
|
||||
|
|
@ -32,7 +33,7 @@ brother_match = re.compile(r"([0-9]*)~(.*)")
|
|||
brothers_matches = [brother_match.match(line) for line in familyfile]
|
||||
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]
|
||||
|
||||
recent_brothers: List[Brother] = brothers[700:]
|
||||
|
||||
async def scroll_callback(slack: SlackClient, msg: dict, match: Match) -> None:
|
||||
"""
|
||||
|
|
@ -81,23 +82,32 @@ class BadName(Exception):
|
|||
"match ratio {}. Please type name better.".format(self.name, self.score, self.threshold)
|
||||
|
||||
|
||||
def find_by_name(name: str, threshold: Optional[float] = None) -> Brother:
|
||||
def find_by_name(name: str, threshold: Optional[float] = None, recent_only: bool=False) -> Brother:
|
||||
"""
|
||||
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 name: The name to look up, with a fuzzy search
|
||||
:param recent_only: Whether or not to only search more recent undergrad brothers.
|
||||
:return: The best-match brother
|
||||
"""
|
||||
# Really quikly mog name into Brother, so the processor function works fine
|
||||
name_bro = Brother(name, -1)
|
||||
# Pick a list
|
||||
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
|
||||
found = process.extractOne(name_bro, brothers, processor=lambda b: b.name)
|
||||
if (not threshold) or found[1] > threshold:
|
||||
return found[0]
|
||||
found, score = process.extractOne(name, all_names)
|
||||
score = score / 100.0
|
||||
if (not threshold) or score > threshold:
|
||||
found_index = all_names.index(found)
|
||||
return bros_to_use[found_index]
|
||||
else:
|
||||
raise BadName(name, found[1], threshold)
|
||||
raise BadName(found, score, threshold)
|
||||
|
||||
|
||||
scroll_hook = slack_util.Hook(scroll_callback, pattern=r"scroll\s+(.*)")
|
||||
|
|
|
|||
|
|
@ -5,14 +5,12 @@ from typing import Any, Optional, Generator, Match, Callable, List, Coroutine
|
|||
from slackclient import SlackClient
|
||||
from slackclient.client import SlackNotConnected
|
||||
|
||||
import channel_util
|
||||
|
||||
"""
|
||||
Slack helpers. Separated for compartmentalization
|
||||
"""
|
||||
|
||||
|
||||
def reply(slack: SlackClient, msg: dict, text: str, in_thread: bool = True, to_channel: str = None) -> None:
|
||||
def reply(slack: SlackClient, msg: dict, text: str, in_thread: bool = True, to_channel: str = None) -> dict:
|
||||
"""
|
||||
Sends message with "text" as its content to the channel that message came from
|
||||
"""
|
||||
|
|
@ -24,9 +22,22 @@ def reply(slack: SlackClient, msg: dict, text: str, in_thread: bool = True, to_c
|
|||
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
|
||||
slack.rtm_send_message(channel=to_channel, message=text, thread=thread)
|
||||
return send_message(slack, text, to_channel, thread=thread)
|
||||
else:
|
||||
slack.rtm_send_message(channel=to_channel, message=text)
|
||||
return send_message(slack, text, to_channel)
|
||||
|
||||
|
||||
def send_message(slack: SlackClient, text: str, channel: str, thread: str = None, broadcast: bool = False) -> dict:
|
||||
"""
|
||||
Copy of the internal send message function of slack
|
||||
"""
|
||||
kwargs = {"channel": channel, "text": text}
|
||||
if thread:
|
||||
kwargs["thread_ts"] = thread
|
||||
if broadcast:
|
||||
kwargs["reply_broadcast"] = True
|
||||
|
||||
return slack.api_call("chat.postMessage", **kwargs)
|
||||
|
||||
|
||||
def im_channel_for_id(slack: SlackClient, user_id: str) -> Optional[str]:
|
||||
|
|
@ -39,29 +50,6 @@ def im_channel_for_id(slack: SlackClient, user_id: str) -> Optional[str]:
|
|||
return None
|
||||
|
||||
|
||||
class SlackDebugCondom(object):
|
||||
def __init__(self, actual_slack: SlackClient):
|
||||
self.actual_slack = actual_slack
|
||||
|
||||
def __getattribute__(self, name: str) -> Any:
|
||||
# Specialized behaviour
|
||||
if name == "rtm_send_message":
|
||||
# Flub some args
|
||||
def override_send_message(*args, **kwargs):
|
||||
print("Overriding: {} {}".format(args, kwargs))
|
||||
kwargs["channel"] = channel_util.BOTZONE
|
||||
kwargs["thread"] = None
|
||||
self.actual_slack.rtm_send_message(*args, **kwargs)
|
||||
|
||||
return override_send_message
|
||||
else:
|
||||
# Default behaviour. Try to give the self first, elsewise give the child
|
||||
try:
|
||||
return super(SlackDebugCondom, self).__getattribute__(name)
|
||||
except AttributeError:
|
||||
return self.actual_slack.__getattribute__(name)
|
||||
|
||||
|
||||
def message_stream(slack: SlackClient) -> Generator[dict, None, None]:
|
||||
"""
|
||||
Generator that yields messages from slack.
|
||||
|
|
@ -93,12 +81,28 @@ MsgAction = Coroutine[Any, Any, None]
|
|||
Callback = Callable[[SlackClient, dict, Match], MsgAction]
|
||||
|
||||
|
||||
class Hook(object):
|
||||
class DeadHook(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AbsHook(object):
|
||||
def __init__(self, consumes_applicable: bool):
|
||||
# Whether or not messages that yield a coroutine should not be checked further
|
||||
self.consumes = consumes_applicable
|
||||
|
||||
def try_apply(self, slack: SlackClient, msg: dict) -> Optional[Coroutine[None, None, None]]:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class Hook(AbsHook):
|
||||
def __init__(self,
|
||||
callback: Callback,
|
||||
pattern: str,
|
||||
channel_whitelist: Optional[List[str]] = None,
|
||||
channel_blacklist: Optional[List[str]] = None):
|
||||
channel_blacklist: Optional[List[str]] = None,
|
||||
consumer: bool = True):
|
||||
super(Hook, self).__init__(consumer)
|
||||
|
||||
# Save all
|
||||
self.pattern = pattern
|
||||
self.channel_whitelist = channel_whitelist
|
||||
|
|
@ -114,7 +118,7 @@ class Hook(object):
|
|||
else:
|
||||
raise Exception("Cannot whitelist and blacklist")
|
||||
|
||||
def check(self, msg: dict) -> Optional[Match]:
|
||||
def try_apply(self, slack: SlackClient, msg: dict) -> Optional[Coroutine[None, None, None]]:
|
||||
"""
|
||||
Returns whether a message should be handled by this dict, returning a Match if so, or None
|
||||
"""
|
||||
|
|
@ -134,9 +138,6 @@ class Hook(object):
|
|||
# print("Hit blacklist")
|
||||
return None
|
||||
|
||||
return match
|
||||
|
||||
def invoke(self, slack: SlackClient, msg: dict, match: Match):
|
||||
return self.callback(slack, msg, match)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
scroll~name
|
||||
0~MISSINGBRO
|
||||
1~Richard Brodeur
|
||||
2~Charles T. Kleman
|
||||
4~Warren Bentley
|
||||
|
|
|
|||
Loading…
Reference in New Issue