-
-
Save nuzayets/df4259101c38f656522726983ab4c5d4 to your computer and use it in GitHub Desktop.
/*** | |
* Automated Vaccine Booker Thing | |
* | |
* I wish everything had an API. | |
* | |
* ____________________ | |
* / PLEASE \ | |
* ! READ ! | |
* ! ENTIRELY ! | |
* \____________________/ | |
* ! ! | |
* ! ! | |
* L_ ! | |
* / _)! | |
* / /__L | |
* _____/ (____) | |
* (____) | |
* _____ (____) | |
* \_(____) | |
* ! ! | |
* ! ! | |
* \__/ | |
* | |
* NOTE - THIS WILL AUTOMATICALLY BOOK WITH NO CONFIRMATION. | |
* It will replace your existing 2nd or booster dose appointment | |
* if you have one. | |
* | |
* 1. You must log in through https://covid19.ontariohealth.ca/booking-home | |
* and be eligible for 2nd or booster dose. | |
* 2. When you select the option that isn't Pharmacy, you are sent to | |
* https://vaccine.covaxonbooking.ca/location-search | |
* You MUST be on this page as pictured: https://i.imgur.com/dfAe1sD.png | |
* 3. Fill in your information below. Your location will be used for the distance | |
* calculation. Your personal info will be used to book the appointment. | |
* 4. Paste the entire file into the browser's Web Developer Tools console. | |
* 5. Output will be in the console. Your confirmation will be in your email on success. | |
*/ | |
// YOUR INFORMATION GOES HERE: | |
// *************************** | |
const YOUR_LOCATION = {lat: 43.646723, lng: -79.413695}; // use Google Maps | |
const MAX_DISTANCE_METERS = 25000; | |
const MIN_DATE = "2022-01-04"; // set to tomorrow's date or later | |
const MAX_DATE = "2022-01-12"; | |
const FIRST_NAME = "FIRSTNAME"; | |
const LAST_NAME = "LASTNAME"; | |
const EMAIL = "[email protected]"; | |
const PHONE = "+14161234567"; // <- in that format | |
// *************************** | |
let cancelToken = { cancelled: false }; | |
let cancel = () => cancelToken.cancelled = true; | |
const getCircularReplacer = () => { | |
const seen = new WeakSet(); | |
return (key, value) => { | |
if (typeof value === "object" && value !== null) { | |
if (seen.has(value)) { | |
return; | |
} | |
seen.add(value); | |
} | |
return value; | |
}; | |
}; | |
const search = (needle, haystack, cr = getCircularReplacer()) => { | |
const found = []; | |
for (const [key, value] of Object.entries(haystack)) { | |
if (typeof value === "object" && value !== null && cr(key,value)) { | |
if (key == needle) found.push(value); | |
const subtree_match = search(needle, value, cr); | |
if (subtree_match) found.push(...subtree_match); | |
} | |
} | |
return found; | |
}; | |
let go = async (c) => { | |
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); | |
const haystack = Object.entries(document.getElementById('root'))[3][1]._internalRoot; | |
const API = search('bookingService', haystack)[1]; | |
const session = search('session', haystack)[1]; | |
const doseNumber = 2; | |
const log = (...o) => console.log(new Date().toLocaleString(), ...o); | |
const find_good_location = async () => { | |
const result = await API.searchLocations({ | |
focusPoint: YOUR_LOCATION, | |
fromDate: new Date, | |
vaccineData: session.vaccineData.latest, | |
locationQuery: session.locationQuery || void 0, | |
doseNumber, | |
includeThirdPartyLocations: true, | |
cursor: "", | |
limit: 20, | |
locationType: "CombinedBooking" | |
}); | |
log("looking..."); | |
const vloc = result.locations.filter( l => l.distanceInMeters <= MAX_DISTANCE_METERS && l.type === "OnlineBooking"); | |
if (vloc.length > 0) return vloc; | |
else { | |
log("no locations found", result) | |
return []; | |
}; | |
}; | |
const check_availability = async (vaxLocation) => { | |
const startDate = new Date(); | |
const endDate = new Date(MAX_DATE); | |
const availabilityResponse = await API.getLocationAvailability(vaxLocation, startDate, endDate, session.vaccineData.latest, doseNumber); | |
const availableDate = availabilityResponse.availability.find( a => a.available && a.date >= MIN_DATE && a.date <= MAX_DATE ); | |
if (!availableDate) { | |
log("no availability at location...", vaxLocation, availabilityResponse); | |
return; | |
} | |
return { location: vaxLocation, date: availableDate.date }; | |
}; | |
let successfullyBooked = false; | |
while (!successfullyBooked) { | |
if (c.cancelled) throw "cancelled"; | |
const locations = await find_good_location(); | |
availability = (await Promise.all(locations.map(check_availability))).find ( x => x ); | |
if (!availability) { | |
continue; | |
} | |
log("got a location & date with availability", availability); | |
const slotsResponse = await API.getLocationSlots(availability.location, availability.date, session.vaccineData.latest); | |
const availableSlot = slotsResponse.slotsWithAvailability.find( x => x ); | |
if (!availableSlot) { | |
log("unable to find an available slot", slotsResponse); | |
continue; | |
} | |
log("found a time slot", availableSlot); | |
const reserveSlotResponse = await API.reserveSlot( | |
doseNumber, | |
availability.location, | |
availability.date, | |
availableSlot.localStartTime, | |
session.vaccineData.latest, | |
"", | |
session.sessionId, | |
1); | |
if (!reserveSlotResponse.value || !reserveSlotResponse.value.reservationId) { | |
log("unable to reserve the timeslot", reserveSlotResponse); | |
continue; | |
} | |
log("got a reserved slot", reserveSlotResponse); | |
const reservationId = reserveSlotResponse.value.reservationId; | |
const personalDetails = { | |
type: "individual-booking", | |
personalDetails: [ | |
{"id": "q.patient.firstname", "value": FIRST_NAME, "type": "text"}, | |
{"id": "q.patient.lastname", "value": LAST_NAME, "type": "text"}, | |
{"id": "q.patient.email", "value": EMAIL, "type": "email"}, | |
{"id": "q.patient.mobile", "value": PHONE, "type": "mobile-phone"} | |
] | |
}; | |
const additionalQuestions = [ | |
{"id": "q.patient.proxy.section.hideshow", "value": "No", "type": "single-select"}, | |
{"id": "q.patient.desc.proxy.name", "type": "text"}, | |
{"id": "q.patient.desc.proxy.phone", "type": "text"}, | |
{"id": "q.patient.desc.relationship.to.the.client", "type": "single-select"} | |
]; | |
if (c.cancelled) throw "cancelled"; | |
const createAppointmentResponse = await API.createAppointment({ | |
eligibilityQuestions: [], | |
personalDetails: personalDetails, | |
additionalQuestions: additionalQuestions, | |
appointments: [ { reservationIds: [ reservationId ] } ], | |
locale: "en-CA", | |
externalAppointments: session.externalAppointments, | |
locationQuery: session.locationQuery, | |
}); | |
log("Success! Check your email.", createAppointmentResponse); | |
successfullyBooked = true; | |
} | |
} | |
try { | |
await go(cancelToken); | |
} catch (error) { | |
console.error("an unexpected error occurred. try logging in again " + | |
"if the following error doesn't mean anything:\n", error); | |
} | |
// Type cancel(); in the console to stop. |
Awesome work! Unfortunately, this script now seems to throws an error (parcelRequire is not defined
). Boosters are in demand and I assume the web app has been updated in the meantime. I don't know much about Angular, so I can only hope somebody will adjust this script 😅
@laenger They blocked the script. Bummer.
I made some changes and got the script working up to the point of calling createAppointment
, but it does require some manual steps that I haven't figured out how to automate yet. The call to createAppointment
fails with an error message saying that it's expecting 'group-booking' instead of 'individual-booking'. I have not been able to figure out why it keeps expecting 'group-booking'. However, I used the logs to at least identify which sites advertise availability and actually have time slots (for some reason that is often not the case), and just focused my manual search on those sites. Got an appointment Jan. 2.
@normtown my JS knowledge is very limited, but I got the script working using only raw fetch
, after I observed a full successful booking in the dev console. See my fork. Definitely not the nicest approach, and some manual preparation needed, but it worked for me (appointment already happened). Maybe these requests help you understand the new "group booking" mechanism? I also revised the search algorithm to check availability for all found locations and not just the first.
@laenger Nice work.
@laenger Are you able to re-post your gist (if you're comfortable with it)? I was going to get around to modifying my version of the script today using yours as a reference to help guide fixes, but found a 404 where your gist used to be.
@normtown updated the link above
Thank you for your work!
I managed to adapt my method of using the shipped API. I can't import the webpack module / chunk directly, but we can fish it out of the virtual DOM nonetheless. The gist has been updated.
It turns out the API actually just returns dates in the past. I've added a feature that allows you to specify a minimum number of
HOURS_IN_THE_FUTURE
to find appointments. So you can do something like "Find me an appointment that's at least 2 hours from now." This also solves the problem I described by filtering out any appointment slots beforeHOURS_IN_THE_FUTURE
from now.