Skip to content

Instantly share code, notes, and snippets.

@nuzayets
Last active January 12, 2024 03:45
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • 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.
@allyellujah
Copy link

Just tried this (at approx 1 PM), it still works! Had to add a MIN_DATE in because it initially booked a slot within 30 minutes, too far away for me to get to - someone must have just cancelled that timeslot.

@nuzayets
Copy link
Author

Just tried this (at approx 1 PM), it still works!

Now I don't know what to believe!

@normtown
Copy link

I also added a MIN_DATE, but it still booked me an appointment for 11:40am yesterday.

@nuzayets
Copy link
Author

new Date() should be timestamp now basically but I am guessing the API somehow truncates it? So if you had set the time to be 0000Z aka midnight UTC on your min date, if they then convert to Eastern on the backend, the date becomes yesterday's date (and the time, 20:00). If they then truncate it, that would get you yesterday's date.

I assumed the API wouldn't return dates in the past if I just give it the current timestamp as the start time but I guess you shouldn't assume too much. Luckily it is easy to just re-run it and re-book if that happens.

If I still had access to the system (which I don't because I have gotten my 2nd dose) then I would try and bugfix it, but since I can't test any changes, I'm reluctant to modify the script.

@normtown
Copy link

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 before HOURS_IN_THE_FUTURE from now.

@laenger
Copy link

laenger commented Dec 21, 2021

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 😅

@StereoTyp0
Copy link

@laenger They blocked the script. Bummer.

@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