2022-05-12 22:28:34 +00:00
|
|
|
import bisect
|
|
|
|
from datetime import datetime, timedelta
|
2022-05-12 22:43:34 +00:00
|
|
|
from flask import Flask, jsonify, make_response, redirect, request
|
2022-05-12 22:28:34 +00:00
|
|
|
from google.auth.transport.requests import Request
|
|
|
|
from google.oauth2.credentials import Credentials
|
|
|
|
from googleapiclient.discovery import build
|
|
|
|
import pytz
|
|
|
|
|
|
|
|
|
|
|
|
# Monday to Friday
|
|
|
|
WEEK_DAYS = {1, 2, 3, 4, 5}
|
|
|
|
|
|
|
|
# 9 to 5
|
|
|
|
FROM = 9
|
|
|
|
TO = 17
|
|
|
|
|
2022-05-12 22:43:34 +00:00
|
|
|
TIMEZONE_NAME = 'America/New_York'
|
|
|
|
TIMEZONE = pytz.timezone(TIMEZONE_NAME)
|
2022-05-12 22:28:34 +00:00
|
|
|
|
|
|
|
AVAILABILITY_LIFETIME = timedelta(minutes=2)
|
|
|
|
|
|
|
|
SCOPES = ['https://www.googleapis.com/auth/calendar']
|
|
|
|
|
|
|
|
UTC = pytz.timezone('UTC')
|
|
|
|
|
|
|
|
creds = Credentials.from_authorized_user_file('creds.json', SCOPES)
|
|
|
|
|
|
|
|
|
|
|
|
def read_datetime(dct):
|
|
|
|
dt = datetime.fromisoformat(dct['dateTime']).replace(tzinfo=None)
|
|
|
|
dt = pytz.timezone(dct['timeZone']).localize(dt)
|
|
|
|
return dt
|
|
|
|
|
|
|
|
|
2022-05-12 22:43:34 +00:00
|
|
|
def asiso(dt):
|
|
|
|
return dt.isoformat()[:19]
|
|
|
|
|
|
|
|
|
2022-05-12 22:28:34 +00:00
|
|
|
def asutciso(dt):
|
2022-05-12 22:43:34 +00:00
|
|
|
return asiso(dt.astimezone(UTC)) + 'Z'
|
2022-05-12 22:28:34 +00:00
|
|
|
|
|
|
|
|
|
|
|
def aslocal(dt):
|
|
|
|
return dt.astimezone(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
|
|
|
|
|
|
|
|
def get_availability():
|
|
|
|
if not creds.valid:
|
|
|
|
creds.refresh(Request())
|
|
|
|
service = build('calendar', 'v3', credentials=creds)
|
|
|
|
|
|
|
|
# Get events for the next 2 weeks
|
|
|
|
now = datetime.utcnow()
|
|
|
|
start = datetime(now.year, now.month, now.day)
|
|
|
|
end = start + timedelta(days=14)
|
|
|
|
events_result = service.events().list(
|
|
|
|
calendarId='primary',
|
|
|
|
timeMin=asutciso(now),
|
|
|
|
timeMax=asutciso(end),
|
|
|
|
maxResults=200,
|
|
|
|
singleEvents=True,
|
|
|
|
orderBy='startTime',
|
|
|
|
).execute()
|
|
|
|
|
|
|
|
# Build a list of possible time slots for the next 2 weeks
|
|
|
|
availability = []
|
|
|
|
for day in range(14):
|
|
|
|
day = start + timedelta(days=day)
|
|
|
|
if day.weekday() not in WEEK_DAYS:
|
|
|
|
continue
|
|
|
|
for hour in range(FROM, TO):
|
|
|
|
for minute in (0, 30):
|
|
|
|
availability.append(TIMEZONE.localize(
|
|
|
|
day.replace(hour=hour, minute=minute),
|
|
|
|
))
|
|
|
|
|
|
|
|
for event in events_result.get('items', []):
|
|
|
|
event_start = read_datetime(event['start'])
|
|
|
|
event_end = read_datetime(event['end'])
|
|
|
|
|
|
|
|
# Find the first slot that starts less than 30min before this event
|
|
|
|
first_idx = bisect.bisect_right(
|
|
|
|
availability,
|
|
|
|
event_start - timedelta(minutes=30),
|
|
|
|
)
|
|
|
|
|
|
|
|
# Find the first slot that starts after this event ends
|
|
|
|
last_idx = bisect.bisect_left(
|
|
|
|
availability,
|
|
|
|
event_end,
|
|
|
|
)
|
|
|
|
|
|
|
|
# Remove the slots from the availability
|
|
|
|
del availability[first_idx:last_idx]
|
|
|
|
|
|
|
|
return availability
|
|
|
|
|
|
|
|
|
|
|
|
availability = get_availability()
|
|
|
|
availability_as_of_date = datetime.utcnow()
|
|
|
|
|
|
|
|
|
|
|
|
app = Flask('calendar_booking_api')
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/availability')
|
|
|
|
def api_availability():
|
|
|
|
global availability, availability_as_of_date
|
|
|
|
if (
|
|
|
|
availability_as_of_date is None
|
|
|
|
or availability_as_of_date + AVAILABILITY_LIFETIME < datetime.utcnow()
|
|
|
|
):
|
|
|
|
availability = get_availability()
|
|
|
|
availability_as_of_date = datetime.utcnow()
|
|
|
|
|
|
|
|
return jsonify({'availability': [asutciso(time) for time in availability]})
|
|
|
|
|
|
|
|
|
2022-05-12 22:43:34 +00:00
|
|
|
@app.route('/book', methods=['POST', 'OPTIONS'])
|
2022-05-12 22:28:34 +00:00
|
|
|
def api_book():
|
2022-05-12 22:43:34 +00:00
|
|
|
# Allow cross-origin
|
|
|
|
if request.method == 'OPTIONS':
|
|
|
|
response = make_response()
|
|
|
|
response.headers.add(
|
|
|
|
'Access-Control-Allow-Origin',
|
|
|
|
'https://vicky.rampin.org',
|
|
|
|
)
|
|
|
|
response.headers.add(
|
|
|
|
'Access-Control-Allow-Headers',
|
|
|
|
'Content-Type',
|
|
|
|
)
|
|
|
|
response.headers.add(
|
|
|
|
'Access-Control-Allow-Methods',
|
|
|
|
'POST',
|
|
|
|
)
|
|
|
|
return response
|
|
|
|
|
|
|
|
if not creds.valid:
|
|
|
|
creds.refresh(Request())
|
|
|
|
service = build('calendar', 'v3', credentials=creds)
|
|
|
|
|
|
|
|
full_name = request.form['name']
|
|
|
|
email = request.form['email']
|
|
|
|
topic = request.form['topic']
|
|
|
|
date = request.form['date']
|
|
|
|
|
|
|
|
end = start + timedelta(minutes=30)
|
|
|
|
|
|
|
|
service.events().insert(
|
|
|
|
calendarId='primary',
|
|
|
|
body=dict(
|
|
|
|
start={'dateTime': asiso(start), 'timeZone': TIMEZONE_NAME},
|
|
|
|
end={'dateTime': asiso(end), 'timeZone': TIMEZONE_NAME},
|
|
|
|
summary="Meeting with Vicky",
|
|
|
|
description="Meeting scheduled from the web",
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
response = redirect('https://vicky.rampin.org/book-successful', 303)
|
|
|
|
response.headers.add(
|
|
|
|
'Access-Control-Allow-Origin',
|
|
|
|
'https://vicky.rampin.org',
|
|
|
|
)
|
|
|
|
return response
|