first draft completed
This commit is contained in:
@@ -34,6 +34,15 @@ def bot():
|
||||
response = f'Okay, I added {sitter_name.title()} to sitters, with phone # {sitter_num}. '
|
||||
print(sitters)
|
||||
|
||||
elif any(remove_word in body for remove_word in ['remove', 'delete']):
|
||||
|
||||
try:
|
||||
sitter_name = remove_sitter(body)
|
||||
except KeyError:
|
||||
response = 'No such sitter. Please write "delete [sitter\'s first name]."'
|
||||
else:
|
||||
response = f'Okay, I removed {sitter_name.title()} from the sitters.'
|
||||
|
||||
resp = MessagingResponse()
|
||||
resp.message(response)
|
||||
return str(resp)
|
||||
@@ -55,10 +64,22 @@ def add_sitter(body: str) -> Tuple[str, str]:
|
||||
|
||||
assert len(num_only) == 10
|
||||
|
||||
sitters[lowercase_name] = f'+1{num_only}'
|
||||
phone_number = f'+1{num_only}'
|
||||
sitters[lowercase_name] = {'num': phone_number,
|
||||
'name': lowercase_name}
|
||||
persist_sitters()
|
||||
|
||||
return name, sitters[lowercase_name]
|
||||
return name, phone_number
|
||||
|
||||
|
||||
def remove_sitter(body: str) -> str:
|
||||
sitter_first_name = body.split(' ')[1]
|
||||
sitter = sitters.get(sitter_first_name)
|
||||
if sitter is None:
|
||||
raise KeyError
|
||||
del sitters[sitter_first_name]
|
||||
persist_sitters()
|
||||
return sitter_first_name
|
||||
|
||||
|
||||
def persist_sitters():
|
||||
|
||||
@@ -5,6 +5,7 @@ click==6.7
|
||||
decorator==4.0.11
|
||||
falcon==1.2.0
|
||||
Flask==0.12.2
|
||||
future==0.16.0
|
||||
google-api-python-client==1.6.2
|
||||
httplib2==0.10.3
|
||||
hug==2.3.0
|
||||
@@ -16,6 +17,7 @@ jedi==0.10.2
|
||||
Jinja2==2.9.6
|
||||
MarkupSafe==1.0
|
||||
oauth2client==4.1.1
|
||||
parsedatetime==2.4
|
||||
parso==0.1.0
|
||||
pexpect==4.2.1
|
||||
pickleshare==0.7.4
|
||||
|
||||
293
sitter_bot.py
293
sitter_bot.py
@@ -1,8 +1,12 @@
|
||||
import datetime
|
||||
from multiprocessing import Process
|
||||
import os
|
||||
from typing import Optional, Tuple
|
||||
import time
|
||||
from typing import Tuple, Dict
|
||||
|
||||
import parsedatetime as pdt
|
||||
import pickle
|
||||
|
||||
from flask import request, Flask
|
||||
from twilio.rest import Client as TwilioClient
|
||||
from twilio.twiml.messaging_response import MessagingResponse
|
||||
@@ -14,31 +18,26 @@ BOOKER_NUM = os.getenv('MY_TWILIO_NUM')
|
||||
COUNTRY_CODE = f'+{os.getenv("TWILIO_COUNTRY_CODE")}'
|
||||
TIMEOUT_MINUTES = 120
|
||||
|
||||
sitters = {}
|
||||
if os.path.exists('sitters.p'):
|
||||
sitters = pickle.load(open('sitters.p', 'rb'))
|
||||
|
||||
bookings = {}
|
||||
if os.path.exists('bookings.p'):
|
||||
bookings = pickle.load(open('bookings.p', 'rb'))
|
||||
|
||||
help_add = 'You can add a sitter by giving me their first name and 10-digit phone number'
|
||||
help_text = help_add + ', or book a sitter by ' \
|
||||
'specifying a date and time. You can also remove a sitter from the list ' \
|
||||
'with "delete" or "remove" and then their first name.'
|
||||
'specifying a date and time. You can also remove a sitter from the list ' \
|
||||
'with "delete" or "remove" and then their first name.'
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(__name__)
|
||||
|
||||
|
||||
class NoneAvailable(Exception):
|
||||
pass
|
||||
cal = pdt.Calendar()
|
||||
|
||||
|
||||
def say_hi_ask_for_sitters():
|
||||
message = "Hey, can you tell me your sitters' info one at a time? " \
|
||||
"First name then phone num. By the way, if you need help, type 'halp' at any time"
|
||||
twilio_client.api.account.messages.create(to=MY_CELL, from_=BOOKER_NUM, body=message)
|
||||
def load_from_pickle(var_name: str) -> dict:
|
||||
payload = {}
|
||||
if os.path.exists(f'{var_name}.p'):
|
||||
payload = pickle.load(open(f'{var_name}.p', 'rb'))
|
||||
return payload
|
||||
|
||||
|
||||
sitters, bookings = load_from_pickle('sitters'), load_from_pickle('bookings')
|
||||
sitters_num_name_lookup = {v['num']: k for k, v in sitters.items()}
|
||||
|
||||
|
||||
@app.route('/bot', methods=['POST'])
|
||||
@@ -47,66 +46,222 @@ def bot() -> str:
|
||||
body = request.values.get('Body').lower()
|
||||
|
||||
resp = MessagingResponse()
|
||||
response = None
|
||||
response = ''
|
||||
|
||||
if not from_ == MY_CELL:
|
||||
return str(resp.message(''))
|
||||
if from_ == MY_CELL:
|
||||
|
||||
if has_phone_num(body):
|
||||
|
||||
try:
|
||||
sitter_name, sitter_num = add_sitter(body)
|
||||
except (AssertionError, ValueError):
|
||||
response = 'Sorry, did you mean to add a sitter? Please try again.'
|
||||
else:
|
||||
response = f'Okay, I added {sitter_name.title()} to sitters, with phone # {sitter_num}. '
|
||||
|
||||
elif any(remove_word in body for remove_word in ['remove', 'delete']):
|
||||
|
||||
try:
|
||||
sitter_name = remove_sitter(body)
|
||||
except KeyError:
|
||||
response = 'No such sitter. Please write "delete [sitter\'s first name]."'
|
||||
else:
|
||||
response = f'Okay, I removed {sitter_name.title()} from the sitters.'
|
||||
|
||||
if 'halp' in body:
|
||||
if not sitters:
|
||||
response = f'You don\'t have any sitters yet. {help_add}.'
|
||||
else:
|
||||
sitter_list = 'Your sitters are ' + ' and '.join(
|
||||
f'{sitter_name.title()}' for sitter_name in sitters) + '.'
|
||||
response = f'{sitter_list} {help_text}'
|
||||
try:
|
||||
start_time, end_time = request_booking(body)
|
||||
except ValueError:
|
||||
response = 'Please specify an end time (e.g. "tomorrow 5pm to 10pm").'
|
||||
else:
|
||||
booking_string = make_booking_string(start_time, end_time)
|
||||
response = f'Okay, I will reach out to the sitters about sitting on {booking_string}.'
|
||||
|
||||
elif has_phone_num(body):
|
||||
|
||||
try:
|
||||
sitter_name, sitter_num = add_sitter(body)
|
||||
except (AssertionError, ValueError):
|
||||
response = 'Sorry, did you mean to add a sitter? Please try again.'
|
||||
else:
|
||||
response = f'Okay, I added {sitter_name.title()} to sitters, with phone # {sitter_num}. '
|
||||
|
||||
elif any(remove_word in body for remove_word in ['remove', 'delete']):
|
||||
|
||||
try:
|
||||
sitter_name = remove_sitter(body)
|
||||
except KeyError:
|
||||
response = 'No such sitter. Please write "delete [sitter\'s first name]."'
|
||||
else:
|
||||
response = f'Okay, I removed {sitter_name.title()} from the sitters.'
|
||||
if response is None:
|
||||
response = 'I wasn\'t sure what to do with your input. ' + help_text
|
||||
|
||||
else:
|
||||
try:
|
||||
book_sitter(body)
|
||||
except (AssertionError, ValueError):
|
||||
response = 'Sorry, did you mean to book a sitter? Please try again.'
|
||||
|
||||
if response is None:
|
||||
response = 'I wasn\'t sure what to do with your input. ' + help_text
|
||||
sitter_name = sitters_num_name_lookup.get(from_)
|
||||
if sitter_name is not None:
|
||||
response = accept_or_decline(sitter_name, body)
|
||||
|
||||
resp.message(response)
|
||||
|
||||
return str(resp)
|
||||
|
||||
|
||||
def accept_or_decline(sitter_name: str, body: str) -> str:
|
||||
body = body.strip()
|
||||
|
||||
global bookings
|
||||
global sitters
|
||||
bookings, sitters = load_from_pickle('bookings'), load_from_pickle('sitters')
|
||||
sitter = sitters[sitter_name]
|
||||
|
||||
sitter_offers = [k for k, v in bookings.items()
|
||||
if sitter_name in v['offered']
|
||||
if v['offered'][sitter_name] not in ['yes', 'no']]
|
||||
|
||||
if body not in ['yes', 'no', 'n', 'y'] and not body.isnumeric():
|
||||
return f'Hm, I\'m not sure what you meant, {sitter_name.title()}. Please write "yes", "no", ' \
|
||||
f'or a number (if there are any pending bookings).'
|
||||
|
||||
action = None
|
||||
|
||||
if not body.isnumeric():
|
||||
action = 'accept' if body in ['yes', 'y'] else 'decline'
|
||||
|
||||
if len(sitter_offers) == 0:
|
||||
return f'Sorry, {sitter_name.title()}, it looks like either that gig ' \
|
||||
f'is already booked or there aren\'t any pending gigs.'
|
||||
|
||||
elif len(sitter_offers) == 1:
|
||||
offer = sitter_offers[0]
|
||||
|
||||
else:
|
||||
sitter_offers_string = ", ".join([f'{idx + 1}) {make_booking_string(*sitter_offer)}'
|
||||
for idx, sitter_offer in enumerate(sitter_offers)])
|
||||
|
||||
if body.isnumeric():
|
||||
try:
|
||||
offer = sitter_offers[int(body) - 1]
|
||||
except IndexError:
|
||||
action = sitter['next action']
|
||||
return f'Sorry, which booking did you want to {action}? {sitter_offers_string}'
|
||||
else:
|
||||
sitter['next action'] = action
|
||||
persist_sitters()
|
||||
return f'Sorry, which booking did you want to {action}? {sitter_offers_string}'
|
||||
|
||||
try:
|
||||
action = action or sitter.pop('next action')
|
||||
except KeyError:
|
||||
raise KeyError(f'no next action, and sitter_offers is {sitter_offers}, so offer is {offer}.')
|
||||
booking_string = make_booking_string(*offer)
|
||||
|
||||
if action == 'accept':
|
||||
|
||||
if not bookings.get(offer):
|
||||
return f'Sorry, {sitter_name.title()}, it looks like {booking_string} is already booked.'
|
||||
|
||||
if any(bookings[offer]['offered'][sitter_] == 'yes'
|
||||
for sitter_ in bookings[offer]['offered'].keys()):
|
||||
if bookings[offer]['offered'][sitter_name] == 'yes':
|
||||
return f'You already accepted {booking_string}, {sitter_name.title()}!'
|
||||
return f'Sorry, {sitter_name.title()}, it looks like {booking_string} is already booked.'
|
||||
|
||||
bookings[offer]['offered'][sitter_name] = 'yes'
|
||||
persist_bookings()
|
||||
update_client(f'{sitter_name.title()} agreed to babysit on {booking_string}!')
|
||||
return f'Awesome, {sitter_name.title()}! See you on {booking_string}.'
|
||||
|
||||
else:
|
||||
if bookings[offer]['offered'][sitter_name] == 'yes':
|
||||
return f'You already accepted {booking_string}, {sitter_name.title()}!'
|
||||
|
||||
bookings[offer]['offered'][sitter_name] = 'no'
|
||||
persist_bookings()
|
||||
return f'Okay, no problem, {sitter_name.title()}! Next time.'
|
||||
|
||||
|
||||
def make_booking_string(start_time: datetime.datetime, end_time: datetime.time) -> str:
|
||||
start_time_and_date_string = start_time.strftime('%-m/%-d from %-I:%M%p')
|
||||
end_time_string = end_time.strftime('%-I:%M%p')
|
||||
return f'{start_time_and_date_string} to {end_time_string}'
|
||||
|
||||
|
||||
def book_forever():
|
||||
while True:
|
||||
|
||||
sitters_, bookings_ = load_from_pickle('sitters'), load_from_pickle('bookings')
|
||||
|
||||
if bookings_:
|
||||
|
||||
bookings_keys_to_delete = []
|
||||
|
||||
for booking_start_and_end, offered_dict in bookings_.items():
|
||||
|
||||
booking = bookings_[booking_start_and_end]
|
||||
offers = booking['offered']
|
||||
|
||||
if any(v == 'yes' for k, v in offers.items()):
|
||||
continue
|
||||
|
||||
sitter_to_offer_name = None
|
||||
booking_string = make_booking_string(*booking_start_and_end)
|
||||
|
||||
if len(offers) == 0:
|
||||
first_sitter_name = list(sitters_)[0]
|
||||
sitter_to_offer_name = first_sitter_name
|
||||
else:
|
||||
last_offer_was_minutes_ago = 0
|
||||
offers_without_a_no = {k: v for k, v in offers.items() if v != 'no'}
|
||||
if len(offers_without_a_no) > 0:
|
||||
last_offer: datetime.datetime = max(offers_without_a_no.values())
|
||||
last_offer_was_minutes_ago \
|
||||
= (datetime.datetime.now() - last_offer).total_seconds() / 60
|
||||
|
||||
if len(offers_without_a_no) == 0 or last_offer_was_minutes_ago > 1:
|
||||
# if len(offers_without_a_no) == 0 or last_offer_was_minutes_ago > 60:
|
||||
|
||||
# if len(sitters_) == len(offers):
|
||||
# bookings_keys_to_delete.append(booking_start_and_end)
|
||||
# update_client(
|
||||
# f'No babysitters are available for {booking_string}! Deleting request.')
|
||||
|
||||
# else:
|
||||
for sitter in sitters_:
|
||||
if sitter not in offers:
|
||||
sitter_to_offer_name = sitter
|
||||
break
|
||||
|
||||
if sitter_to_offer_name is not None:
|
||||
sitter_to_offer = sitters_[sitter_to_offer_name]
|
||||
offer_booking(sitter_to_offer, booking_string)
|
||||
offers[sitter_to_offer_name] = datetime.datetime.now()
|
||||
update_client(
|
||||
f'Okay, I offered {booking_string} to {sitter_to_offer_name.title()}.')
|
||||
|
||||
if len(bookings_keys_to_delete) > 0:
|
||||
for k in bookings_keys_to_delete:
|
||||
del bookings_[k]
|
||||
|
||||
persist_bookings(bookings_)
|
||||
|
||||
# time.sleep(60)
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
def offer_booking(sitter_dict: dict, booking_string: str) -> None:
|
||||
message = f'{sitter_dict["name"].title()}, are you available to babysit on {booking_string}?'
|
||||
twilio_client.api.account.messages.create(to=sitter_dict['num'],
|
||||
from_=BOOKER_NUM,
|
||||
body=message)
|
||||
|
||||
|
||||
def update_client(string: str) -> None:
|
||||
twilio_client.api.account.messages.create(to=MY_CELL, from_=BOOKER_NUM, body=string)
|
||||
|
||||
|
||||
def has_phone_num(string):
|
||||
return len([char for char in string if char.isnumeric()]) == 10
|
||||
|
||||
|
||||
def syndicate_and_book(session_start: datetime.datetime, session_end: datetime.datetime) -> Optional[str]:
|
||||
# blast out to all sitters
|
||||
# give it to the first 'yes'
|
||||
# handle late 'yeses' and all 'noes'
|
||||
# wait until everyone says no or timeout_minutes runs out
|
||||
pass
|
||||
def request_booking(body: str) -> Tuple[datetime.datetime, datetime.time]:
|
||||
session_start, session_end = parse_booking_request(body)
|
||||
global bookings
|
||||
bookings = load_from_pickle('bookings')
|
||||
bookings[(session_start, session_end)] = {'offered': dict()}
|
||||
persist_bookings()
|
||||
return session_start, session_end
|
||||
|
||||
|
||||
def book_sitter(body: str) -> Optional[str]:
|
||||
session_start, session_end = parse_sitter_request(body)
|
||||
syndicate_and_book(session_start, session_end)
|
||||
def parse_booking_request(body: str) -> Tuple[datetime.datetime, datetime.time]:
|
||||
start_string, end_string = body.split(' to ')
|
||||
session_start = cal.parseDT(start_string)[0]
|
||||
session_end_time = datetime.time(cal.parse(end_string)[0].tm_hour)
|
||||
return session_start, session_end_time
|
||||
|
||||
|
||||
def add_sitter(body: str) -> Tuple[str, str]:
|
||||
@@ -120,9 +275,12 @@ def add_sitter(body: str) -> Tuple[str, str]:
|
||||
|
||||
assert len(num_only) == 10
|
||||
|
||||
sitters[lowercase_name] = f'{COUNTRY_CODE}num_only'
|
||||
phone_number = f'{COUNTRY_CODE}{num_only}'
|
||||
sitters[lowercase_name] = {'num': phone_number,
|
||||
'name': lowercase_name}
|
||||
|
||||
persist_sitters()
|
||||
return name, sitters[lowercase_name]
|
||||
return name, phone_number
|
||||
|
||||
|
||||
def remove_sitter(body: str) -> str:
|
||||
@@ -139,7 +297,14 @@ def persist_sitters():
|
||||
pickle.dump(sitters, open('sitters.p', 'wb'))
|
||||
|
||||
|
||||
def persist_bookings(bookings_: dict = None):
|
||||
pickle.dump(bookings_ if bookings_ is not None else bookings, open('bookings.p', 'wb'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if not sitters:
|
||||
say_hi_ask_for_sitters()
|
||||
app.run(debug=True, port=4567)
|
||||
update_client('Hi, this is Babysitter Bot, on the job! Send me a date with time range and '
|
||||
'I\'ll try to book one of our sitters!')
|
||||
p = Process(target=book_forever)
|
||||
p.start()
|
||||
app.run(debug=True, port=8000)
|
||||
p.join()
|
||||
|
||||
Reference in New Issue
Block a user