Last active
April 14, 2021 19:29
-
-
Save samliu/26cc049a8d222277016bdbd75917288c to your computer and use it in GitHub Desktop.
Find CVS appointments for COVID-19 vaccine, and get them texted to your phone.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""check_vaccine.py | |
Checks CVS for vaccine appointment availability and sends text messages if they show up. | |
Requires paid account on Twilio. You can fund $5 and it'll be plenty, a SMS is less than 1 cent. | |
Assuming you use a *nix system with Python 3 and want to deploy via crontab, you can do something like this: | |
1. `which python` Record the output. (It's nice to give crontab the full path of your python binary.) | |
2. `export VISUAL=vim` (For `crontab -e` to use vim; my personal preference.) | |
3. `crontab -e` to create a new line in your crontab. Next line is an example: | |
4. `* * * * * /home/$USER/.virtualenvs/python3.7/bin/python /home/$USER/check_vaccine.py >> /home/$USER/vaccine_check.log` | |
(Run the script every minute and log the output) | |
5. `tail -f /home/$USER/vaccine_check.log` (For if you want to just see the messages getting printed to stdout.) | |
Alternatively just add some logic to do infinite loop + sleep between calls to `find_appointments()`. | |
If you want the dependencies, write the following into `requirements.txt`: | |
python>=3.4 | |
pytz>=2019.3 | |
requests>=2.23.0 | |
twilio>=6.53.0 | |
...then `pip install -r requirements.txt` | |
""" | |
import requests | |
import json | |
import time | |
import os | |
import pathlib | |
import pytz | |
from datetime import datetime | |
from datetime import timedelta | |
from twilio.rest import Client | |
class bcolors: | |
"""For printing in color.""" | |
WARNING = '\033[93m' | |
ENDC = '\033[0m' | |
class CVSVaccineChecker(object): | |
def __init__(self, twilio_account_sid, twilio_auth_token, | |
from_number='+16505551234', | |
to_numbers=None, | |
state='NJ', | |
timezone=pytz.timezone('US/Eastern'), rate_limit_minutes=10, | |
city_skip_list=None, verbose=True): | |
"""CVS Vaccine Checker. | |
twilio_account_id: string, credential from twilio. SMS is cheap, fund $5 and | |
you should be fine. | |
twilio_auth_token: string, another credential from twilio. | |
from_number: string, phone number you purchased from twilio for sending. | |
Expected format is `+<countrycode><phone number>`. | |
to_numbers: list of strings. Same format as above, one for each number you | |
want to notify. | |
state: string, CVS doesn't support every state so check their website. | |
timezone: pytz timezone object representing the timezone you want. Default | |
is US eastern time. | |
rate_limit_minutes: number of minutes to rate limit SMS notifications. Since | |
the site's availability info lags, you don't want to keep getting | |
notifications even after the spots have been booked. | |
city_skip_list: list of strings representing cities you want to skip / not | |
notify on. | |
verbose: bool, whether to print outputs to stdout. | |
""" | |
self.client = Client(twilio_account_sid, twilio_auth_token) | |
self.from_number = from_number | |
self.to_numbers = [] | |
if self.to_numbers: | |
self.to_numbers = to_numbers | |
self.state = state | |
self.tz = timezone | |
self.rate_limit_minutes = rate_limit_minutes | |
self.city_skip_list = [] | |
if city_skip_list: | |
self.city_skip_list = city_skip_list | |
self.verbose = verbose | |
# Create the last updated timestamp file if it doesn't exist. | |
self.last_updated_file = pathlib.Path("last_updated.ts") | |
if not self.last_updated_file.exists(): | |
f = open(self.last_updated_file, 'w') | |
f.write(str(datetime.now())) | |
f.close() | |
def __maybe_print(self, s): | |
if self.verbose: | |
print(s) | |
def __send_sms(self, msg, dest_numbers=None): | |
"""Send an SMS notification using twilio.""" | |
if type(dest_numbers) is not list: | |
raise Exception('Must specify numbers to send text messages to') | |
for dest_number in dest_numbers: | |
message = self.client.messages \ | |
.create( | |
body=msg, | |
from_=self.from_number, | |
to=dest_number, | |
) | |
self.__maybe_print(f'Sent message to {message.sid}') | |
def find_appointments(self): | |
available_cities = [] | |
try: | |
# Download json from CVS website. | |
url = ('https://www.cvs.com//immunizations/covid-19-vaccine.vaccine' | |
f'-status.{self.state}.json?vaccineinfo') | |
r = requests.get( | |
url, | |
headers={ | |
'referer': 'https://www.cvs.com/immunizations/covid-19-vaccine'}) | |
data = r.content | |
self.__maybe_print(data) | |
except Exception as e: | |
self.__maybe_print(f'fuq it didn\'t work. {str(e)}') | |
obj = json.loads(data) | |
del obj["responseMetaData"] # Remove entry to make iteration easier. | |
# Check availability. Status options are: ["Fully Booked", "Available"]. | |
for _, val in obj.items(): | |
for i in range(0, len(val['data'][self.state])): | |
# If there is availability anywhere in the state, take some action. | |
if val['data'][self.state][i]['status'] == 'Available': | |
city_name = str(val['data'][self.state][i]['city']).title() | |
if city_name not in self.city_skip_list: | |
available_cities.append(city_name) | |
now = datetime.now(self.tz) | |
strDateTime = now.strftime('%m/%d/%Y %I:%M %p') | |
self.__maybe_print(f'{len(available_cities)} cities have slots open as of ' | |
f'{strDateTime} {self.tz}') | |
# Send messages if necessary. | |
if len(available_cities) > 0: | |
# Janky rate limit logic using a flat file. | |
rate_limited = False | |
with open(self.last_updated_file) as file: | |
last_updated = datetime.strptime( | |
file.readlines()[0], '%Y-%m-%d %H:%M:%S.%f') | |
delta = datetime.now() - last_updated | |
if delta >= timedelta(minutes=self.rate_limit_minutes): | |
f = open(self.last_updated_file, 'w') | |
f.write(str(datetime.now())) | |
f.close() | |
else: | |
# If we get here, it's been less than five minutes since we last sent a | |
# message, so let's rate limit and skip sending. | |
rate_limited = True | |
self.__maybe_print('Skipping send, time delta since last notification' | |
f'was {str(delta)}') | |
if not rate_limited: | |
nums = self.to_numbers | |
msg = (' We found appointments available in ' + | |
', '.join(available_cities) + | |
'! Book now: https://www.cvs.com/vaccine/intake/store/' | |
'eligibility-screener/eligibility-covid') | |
self.__send_sms(msg, | |
dest_numbers=nums) | |
# Open a browser window too. This is so custom to what browser you use | |
# and where you installed it that I'm leaving it commented out. | |
# | |
# reserve_url = ('https://www.cvs.com/vaccine/intake/store' | |
# '/eligibility-screener/eligibility-covid') | |
# import webbrowser | |
# chrome_path = '/usr/bin/google-chrome %s' | |
# webbrowser.get(chrome_path).open(reserve_url) | |
self.__maybe_print(f'{bcolors.WARNING}Availability in these cities:' | |
f' {", ".join(available_cities)} {bcolors.ENDC}') | |
self.__maybe_print('--------------------------------\n') # Spacer. | |
if __name__ == "__main__": | |
# Example of how to use this. | |
checker = CVSVaccineChecker( | |
twilio_account_sid='', | |
twilio_auth_token='', | |
from_number='+16505551234', | |
to_numbers=['+16505554321'], | |
state='NJ') | |
# Instead of cron you could make this a long lived process and call this between sleep(). | |
checker.find_appointments() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment