A Twitter Bot for COVID Vaccine Availability in New York City
The other day I stopped to think about the collective time and energy that’s been wasted from people refreshing vaccine booking portals trying to book appointments for themselves or their loved ones.
A few months ago when COVID vaccines were first becoming available in New York, I built a web scraper to constantly check the Walgreens, CVS, and New York State vaccine portals for available appointments. If it found one, it would send me an SMS via the Twilio API. This was incredibly useful, and I managed to snag at least one appointment for a family member early on. But then I let it sit for a few weeks. As you can imagine, vaccine distributers are constantly updating and changing their booking systems whether it be to fix issues or to fit the ever-changing demands. This ultimately led to my script no longer working well enough to be useful.
I decided to do some poking around to see if there was a better way and not long after, I came across the ArcGIS page for the API that powers vaccinefinder.nyc.gov. It didn’t take long after that to spin up a Twitter bot to spit out the information.
Vaccine appointments are available at
— NYC Vaccine Bot (@nyc_vaccine) March 26, 2021
NYC Vaccine Hub - Essex Crossing (Moderna)
244B Broome Street, Manhattan, NY, 10002https://t.co/g7udiCIGur
The code itself is pretty simple. We use node-fetch
to read the vaccine data, and the twit
package to interact with the Twitter API.
const fetch = require('node-fetch'); | |
const Twit = require('twit'); | |
let lastNotifiedLocations = []; | |
const T = new Twit({ | |
consumer_key: process.env.TWITTER_CONSUMER_KEY, | |
consumer_secret: process.env.TWITTER_CONSUMER_SECRET, | |
access_token: process.env.TWITTER_ACCESS_TOKEN, | |
access_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET, | |
}); | |
async function getAppointments() { | |
const response = await fetch("https://services1.arcgis.com/oOUgp466Coyjcu6V/arcgis/rest/services/VaccineFinder_Production_View/FeatureServer/0/query?f=json&cacheHint=true&orderByFields&outFields=*&outSR=4326&spatialRel=esriSpatialRelIntersects&where=1%3D1"); | |
const { features } = await response.json(); | |
const appoints = features | |
.filter(x => x.attributes.AppointmentAvailability === 'Available'); | |
const filtered = appoints | |
.filter(x => lastNotifiedLocations.indexOf(x.attributes.OBJECTID) === -1); | |
lastNotifiedLocations = appoints.map(x => x.attributes.OBJECTID); | |
return filtered; | |
} | |
async function postToTwitter(appointments) { | |
for (let { attributes } of appointments) { | |
const { FacilityName, Address, Address2, Borough, Zipcode, Website, ServiceType_Pfizer, ServiceType_Moderna, ServiceType_JohnsonAndJohnson } = attributes; | |
const brand = ServiceType_Moderna === "Yes" ? 'Moderna' : (ServiceType_Pfizer === "Yes" ? 'Pfizer' : (ServiceType_JohnsonAndJohnson === "Yes" ? 'J&J' : null)); | |
await T.post('statuses/update', { | |
status: `Vaccine appointments are available at\n\n${FacilityName}${brand ? ` (${brand})` : ''}\n${Address}${Address2?.length > 0 ? `, ${Address2}` : ''}, ${Borough}, NY, ${Zipcode}\n\n${Website}`, | |
}); | |
} | |
} | |
async function run() { | |
const appointments = await getAppointments(); | |
console.log('[INFO]', `Found appointments at ${appointments.length} locations.`); | |
if (appointments.length === 0) return; | |
await postToTwitter(appointments); | |
} | |
try { | |
(async () => { | |
console.log('💉 vaccine-bot is running!'); | |
setInterval(async () => { | |
await run(); | |
}, 300000); | |
await run(); | |
})(); | |
} catch (error) { | |
console.log(error); | |
} |
I’m running this in Docker on a node 14.x image using docker-compose
.
FROM node:14 | |
WORKDIR /usr/src/app | |
COPY package*.json ./ | |
ENV NODE_ENV production | |
RUN npm ci --only=production | |
COPY . . | |
EXPOSE 3000 | |
CMD [ "node", "app.js" ] |
version: "3" | |
services: | |
vaccine-bot: | |
build: | |
context: . | |
dockerfile: Dockerfile | |
environment: | |
- TWITTER_CONSUMER_KEY= | |
- TWITTER_CONSUMER_SECRET= | |
- TWITTER_ACCESS_TOKEN= | |
- TWITTER_ACCESS_TOKEN_SECRET= |