Skip to content

Instantly share code, notes, and snippets.

@nuzayets
Last active January 12, 2024 03:45
Show Gist options
  • Save nuzayets/df4259101c38f656522726983ab4c5d4 to your computer and use it in GitHub Desktop.
Save nuzayets/df4259101c38f656522726983ab4c5d4 to your computer and use it in GitHub Desktop.
Automatically book 2nd & booster dose vaccine at closest location Ontario
/***
* 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@example.com";
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.
@normtown
Copy link

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.

@laenger
Copy link

laenger commented Dec 28, 2021

@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.

@normtown
Copy link

@laenger Nice work.

@normtown
Copy link

normtown commented Jan 2, 2022

@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.

@laenger
Copy link

laenger commented Jan 2, 2022

@normtown updated the link above

@nuzayets
Copy link
Author

nuzayets commented Jan 4, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment