notif-visa-ext/scripts/content.js

639 lines
23 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) 2024, snegov
// All rights reserved.
const pathnameRegex = /^\/\w{2}-\w{2}\/n?iv/;
const MAX_SIGNIN_ATTEMPTS = 1;
const PAGE_WAIT_TIME = 3792;
const MINUTE = 60;
const SOFT_BAN_TIMEOUT = 27;
const NOTIF_CHANNEL = "snegov_test"
let config = {
activate: null,
username: null,
password: null,
frequency: null,
apptId: null,
currentAppt: {
consulate: null,
date: null,
},
signinAttempts: null,
consulates: null,
deltaAppt: null,
deltaNow: null,
};
let isRunning = false;
let msg = "";
let isFoundAppointment = false;
async function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function sendNotification(message, channel = NOTIF_CHANNEL) {
await fetch('https://ntfy.sh/' + channel, {
method: 'POST',
body: "[US visa bot] " + message,
})
.then(() => console.log('Notification sent'))
.catch(e => console.error(e));
}
function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
function getFutureDate(minutes, maxRandomSeconds = 0) {
// return date some amount of minutes in future plus random amount of seconds
let futureDate = new Date();
futureDate.setMinutes(futureDate.getMinutes() + minutes);
futureDate.setSeconds(futureDate.getSeconds() + getRandomInt(maxRandomSeconds));
return futureDate.toISOString();
}
function hiddenPassword(config) {
return {
...config,
password: config.password.replace(/./g, "*"),
};
}
function diffObjects(obj1, obj2) {
let diff = {};
for (let key in obj1) {
if (typeof obj1[key] === 'object' && obj1[key] !== null && obj2[key]) {
let nestedDiff = diffObjects(obj1[key], obj2[key]);
if (Object.keys(nestedDiff).length > 0) {
diff[key] = nestedDiff;
}
} else if (obj2[key] && obj1[key] !== obj2[key]) {
diff[key] = [obj1[key], obj2[key]];
}
}
for (let key in obj2) {
if (obj1[key] === undefined && obj2[key] !== undefined) {
diff[key] = [undefined, obj2[key]];
}
}
return diff;
}
function getPathnameParts(pathname) {
let pathParts = pathname.split('/');
let locale = pathParts[1] || 'en-us';
let visaType = pathParts[2] || 'niv';
return { locale, visaType };
}
function isSignInPage() {
return Boolean(window.location.pathname.match(/^\/\w{2}-\w{2}\/n?iv\/users\/sign_in/));
}
function isLoggedOutPage() {
return Boolean(window.location.pathname.match(/^\/\w{2}-\w{2}\/n?iv$/));
}
function isDashboardPage() {
return Boolean(window.location.pathname.match(/^\/\w{2}-\w{2}\/n?iv\/groups\/\d+/));
}
function isAppointmentPage() {
return Boolean(window.location.pathname.match(/^\/\w{2}-\w{2}\/n?iv\/schedule\/\d+\/appointment$/));
}
function isConfirmationPage() {
return Boolean(window.location.pathname.match(/^\/\w{2}-\w{2}\/n?iv\/schedule\/\d+\/appointment\/instructions$/));
}
function isNotEnglishPage() {
return Boolean(isSignInPage()
|| isLoggedOutPage()
|| isDashboardPage()
|| isAppointmentPage()
|| isConfirmationPage()
) && !window.location.pathname.match(/^\/en-/);
}
async function handleHttpError(e) {
if (e.response && e.response.status === 401) {
msg = "Unauthorized";
console.log(msg);
await sendNotification(msg);
await logOut();
return null;
} else {
console.error(e);
return null;
}
}
async function switchToEnglishPage() {
window.location.href(window.location.pathname.replace(/^\/\w{2}-{2}/, '/en-us'));
await delay(PAGE_WAIT_TIME);
// Should be on English page
}
async function logOut() {
let [locale, visaType] = getPathnameParts(window.location.pathname);
let signOutPath = `/${locale}/${visaType}/users/sign_out`;
window.location.href = window.location.origin + signOutPath;
await delay(PAGE_WAIT_TIME);
return isLoggedOutPage();
}
async function goToSignInPage() {
let [locale, visaType] = getPathnameParts(window.location.pathname);
let signInPath = `/${locale}/${visaType}/users/sign_in`;
window.location.href = window.location.origin + signInPath;
await delay(PAGE_WAIT_TIME);
return isSignInPage();
}
async function goToDashboardPage() {
let [locale, visaType] = getPathnameParts(window.location.pathname);
let dashboardPath = `/${locale}/${visaType}/account`;
window.location.href = window.location.origin + dashboardPath;
await delay(PAGE_WAIT_TIME);
return isDashboardPage();
}
async function enterCredentials() {
document.getElementById("user_email").value = config.username;
document.getElementById("user_password").value = config.password;
let policyConfirmed = document.querySelector('[for="policy_confirmed"]');
if (!policyConfirmed.getElementsByTagName('input')[0].checked) {
policyConfirmed.click();
}
document.querySelector("#sign_in_form input[type=submit]").click();
await delay(PAGE_WAIT_TIME);
return isDashboardPage();
}
async function getAppointmentId() {
let appointments = document.querySelectorAll("p.consular-appt [href]")
if (appointments.length > 1) {
console.log("Multiple appointments found, taking the first one");
}
let apptId = appointments[0].href.replace(/\D/g, "");
return apptId;
}
async function getConsulates() {
let consulatesSelect = document.querySelector("#appointments_consulate_appointment_facility_id")
let consulatesDict = {};
for (let option of consulatesSelect.options) {
if (!option.value) continue; // skip empty option
consulatesDict[option.text] = {
"id": parseInt(option.value),
"isSelected": option.selected,
"bestDate": null,
"currentDate": null,
"nextCheckAt": getFutureDate(0, config.frequency * MINUTE),
"autobook": false,
};
}
return consulatesDict;
}
async function getAvailableDates(consulateId) {
let uri = window.location.pathname + `/days/${consulateId}.json?appointments[expedite]=false`
let dates = fetch(uri, { headers: { "x-requested-with": "XMLHttpRequest" }})
.then(d => d.json())
.then(data => {
let dateList = data.map(item => item.date);
dateList.sort();
return dateList;
})
.catch(async e => handleHttpError(e));
return dates;
}
async function filterDates(dates, currentAppt, deltaFromAppt, deltaFromNow) {
let maxDate = new Date(currentAppt);
// let maxDate = new Date("2029-09-09");
maxDate.setDate(maxDate.getDate() - deltaFromAppt);
let minDate = new Date();
minDate.setDate(minDate.getDate() + deltaFromNow);
let availableDates = dates.filter(d => new Date(d) >= minDate && new Date(d) < maxDate);
return availableDates;
}
async function getAvailableTimes(consulateId, date) {
let uri = window.location.pathname + `/times/${consulateId}.json?date=${date}&appointments[expedite]=false`
let times = await fetch(uri, { headers: { "x-requested-with": "XMLHttpRequest" } })
.then(d => d.json())
.then(data => data.available_times)
.catch(e => handleHttpError(e));
return times;
}
async function runner() {
if (isRunning) {
return;
}
isRunning = true;
// console.log('runner start');
let prev_config = Object.assign({}, config);
let result = await new Promise(resolve => chrome.storage.local.get(null, resolve));
config.activate = result['__activate'] || false;
config.username = result['__username'] || "";
config.password = result['__password'] || "";
config.frequency = parseInt(result['__frequency'] || 1);
config.signinAttempts = result['__signinAttempts'] || 0;
config.apptId = result['__apptId'] || null;
config.currentAppt = result['__currentAppt'] || { consulate: null, date: null };
config.consulates = result['__consulates'] || null;
config.deltaAppt = result['__deltaAppt'] || 1;
config.deltaNow = result['__deltaNow'] || 1;
if (prev_config.activate === null) {
console.log('Reading config: ' + JSON.stringify(hiddenPassword(config)));
isRunning = false;
return;
}
for (let key in config) {
if (config.hasOwnProperty(key)
&& !_.isEqual(config[key], prev_config[key])) {
msg = `Config change: ${key}`
if (key === 'password') {
msg += ', ******** => ********';
} else if (key === 'consulates') {
msg += `, ${JSON.stringify(diffObjects(config[key], prev_config[key]))}`;
} else {
msg += `, ${prev_config[key]} => ${config[key]}`;
}
console.log(msg);
// reduce wait times for consulates if frequency is increased
if (key === 'frequency') {
let wasChanged = false;
for (let consulate in config.consulates) {
let newNextCheckAt = getFutureDate(config.frequency, 10);
if (config.consulates[consulate].nextCheckAt > newNextCheckAt) {
config.consulates[consulate].nextCheckAt = newNextCheckAt;
wasChanged = true;
console.log(`Reducing wait time for ${consulate} from ${config.consulates[consulate].nextCheckAt} to ${newNextCheckAt}`);
}
}
// TODO maybe causes additional requests
if (wasChanged) {
await chrome.storage.local.set({ "__consulates": config.consulates });
}
}
if (key === 'activate') {
if (config[key]) {
console.log('Activating extension');
} else {
console.log('Deactivating extension');
await chrome.storage.local.set({ "__status": "inactive" });
}
}
// clear signin attempts when credentials are changed
if (key === 'username' && config[key]
|| key === 'password' && config[key]) {
config.signinAttempts = 0;
await chrome.storage.local.set({ "__signinAttempts": config.signinAttempts });
}
}
}
if (!config.activate) {
isRunning = false;
return;
}
if (config.username === "" || config.password === "") {
console.log('Username or password is empty');
await chrome.storage.local.set({ "__status": "missing credentials" });
isRunning = false;
return;
}
if (config.frequency <= 0) {
console.log('Frequency is 0 or negative');
await chrome.storage.local.set({ "__status": "invalid frequency" });
isRunning = false;
return;
}
// Check if current time is between 11pm and 9am UTC (4pm - 2am PST)
let now = new Date();
let currentHourUTC = now.getUTCHours();
if (currentHourUTC >= 23 || currentHourUTC < 9) {
// Continue running the code
} else {
isRunning = false;
return;
}
if (isFoundAppointment) {
// don't do anything if appointment is found and manual booking is required
if (isAppointmentPage()) {
isRunning = false;
return;
// we're not on an appointment page, so seems like we're done, and we can check dates again
} else {
isFoundAppointment = false;
}
}
if (isNotEnglishPage()) {
await switchToEnglishPage();
}
if (isLoggedOutPage()) {
if (!await goToSignInPage()) {
msg = 'Failed to go to sign in page';
console.log(msg);
await chrome.storage.local.set({ "__status": msg });
isRunning = false;
return;
};
}
else if (isSignInPage()) {
// Prevent brute forcing
if (config.signinAttempts >= MAX_SIGNIN_ATTEMPTS) {
await chrome.storage.local.set({ "__status": "too many sign in attempts" });
isRunning = false;
return;
}
// Sign in
msg = 'Signing in attempt: ' + config.signinAttempts;
console.log(msg)
await chrome.storage.local.set({ "__status": msg });
let signedIn = await enterCredentials();
config.signinAttempts += 1;
await chrome.storage.local.set({ "__signinAttempts": config.signinAttempts });
if (!signedIn) {
msg = 'Failed to sign in';
console.log(msg);
await chrome.storage.local.set({ "__status": msg });
isRunning = false;
return;
};
}
else if (isDashboardPage()) {
// reset signin attempts when successfully logged in
config.signinAttempts = 0;
await chrome.storage.local.set({ "__status": "fetching appointment info" });
await chrome.storage.local.set({ "__signinAttempts": config.signinAttempts });
// get appointmentId
if (!config.apptId) {
config.apptId = await getAppointmentId();
if (config.apptId) {
console.log(`Appointment ID: ${config.apptId}`);
await chrome.storage.local.set({ "__apptId": config.apptId });
} else {
msg = 'No appointments found';
console.log(msg);
await chrome.storage.local.set({ "__status": msg });
isRunning = false;
return;
}
}
// get current appointment date
let apptInfoCard = document.querySelector("p.consular-appt [href*='" + config.apptId + "']").parentNode.parentNode.parentNode
if (!apptInfoCard.querySelector("h4").innerText.match(/Attend Appointment/)) {
msg = 'Appointment not available';
console.log(msg);
await chrome.storage.local.set({ "__status": msg });
isRunning = false;
return;
}
let apptInfo = apptInfoCard.querySelector("p.consular-appt").innerText;
let apptConsulate = (apptInfo.match(/at (\w+)/) || [])[1];
let apptDate = new Date(apptInfo.match(/\d{1,2} \w+, \d{4}/)[0]);
apptDate = apptDate.toISOString().slice(0, 10);
if (apptDate && apptConsulate
&& (apptDate != config.currentAppt.date
|| apptConsulate != config.currentAppt.consulate)) {
console.log(`New appointment date: ${apptDate} at ${apptConsulate},
old: ${config.currentAppt.consulate} at ${config.currentAppt.date}`);
config.currentAppt.date = apptDate;
config.currentAppt.consulate = apptConsulate;
await chrome.storage.local.set({ "__currentAppt": config.currentAppt });
}
// go to appointment page
let apptLink = apptInfoCard.querySelector("p.consular-appt [href]")
.getAttribute("href").replace("/addresses/consulate", "/appointment");
window.location.href = apptLink;
await delay(PAGE_WAIT_TIME);
}
else if (isAppointmentPage()) {
// if no apptDate, fetch it from dashboard page
if (!config.currentAppt.date) {
console.log('No appointment date is set, going back to dashboard');
await goToDashboardPage();
isRunning = false;
return;
}
// await chrome.storage.local.set({ "__status": "fetching consulates" });
let applicantForm = document.querySelector('form[action*="' + window.location.pathname + '"]');
if (applicantForm && applicantForm.method.toLowerCase() == "get") {
applicantForm.submit();
await delay(PAGE_WAIT_TIME);
isRunning = false;
return;
}
if (!document.getElementById("consulate_date_time")) {
msg = 'No available appointments';
console.log(msg);
await chrome.storage.local.set({ "__status": msg });
isRunning = false;
return;
};
if (!config.consulates) {
config.consulates = await getConsulates();
await chrome.storage.local.set({ "__consulates": config.consulates });
}
if (!config.consulates) {
console.log('No consulates found');
await chrome.storage.local.set({ "__status": "no consulates found" });
isRunning = false;
return;
}
// TODO choose ASC facility
// document.querySelector("#appointments_asc_appointment_facility_id [selected]").innerText
// for each selected consulate check available dates
let selectedConsulates = Object.keys(config.consulates).filter(c => config.consulates[c].isSelected);
if (selectedConsulates.length == 0) {
msg = 'No selected consulates found';
console.log(msg);
await chrome.storage.local.set({ "__status": msg });
isRunning = false;
return;
}
let processedConsulates = 0;
for (let consulate of selectedConsulates) {
// only one consulate per run
if (processedConsulates > 0) {
break;
}
// skip if not time to check
if (config.consulates[consulate].nextCheckAt > new Date().toISOString()) {
continue;
}
processedConsulates += 1;
console.log('Checking dates for ' + consulate);
let availDates = await getAvailableDates(config.consulates[consulate].id);
config.consulates[consulate].nextCheckAt = getFutureDate(config.frequency, 10);
if (!availDates) {
msg = `Failed to fetch available dates in ${consulate}`;
console.log(msg);
await chrome.storage.local.set({ "__status": msg });
continue;
}
// if empty list, either we're banned or non operational hours or dead consulate
// wait for some time before checking again
let now = new Date();
let currentHourUTC = now.getUTCHours();
let currentMinuteUTC = now.getUTCMinutes();
if (availDates.length == 0) {
msg = `No available dates in ${consulate}, probably banned`;
console.log(msg);
await chrome.storage.local.set({ "__status": msg });
// Only set SOFT_BAN_TIMEOUT if it's not the first 5 minutes of 23pm UTC
if (!(currentHourUTC === 23 && currentMinuteUTC < 5)) {
config.consulates[consulate].nextCheckAt = getFutureDate(SOFT_BAN_TIMEOUT, 10);
}
continue;
}
msg = `Available dates for ${consulate}: ${availDates.slice(0, 5)}`;
if (availDates.length > 5) {
msg += ` and ${availDates.length - 10} more`;
}
console.log(msg);
config.consulates[consulate].currentDate = availDates[0];
if (!config.consulates[consulate].bestDate
|| availDates[0] < config.consulates[consulate].bestDate) {
console.log(`New record for ${consulate}: ${availDates[0]}`);
config.consulates[consulate].bestDate = availDates[0];
}
// filter dates with our requests
let filteredDates = await filterDates(availDates, config.currentAppt.date, config.deltaAppt, config.deltaNow);
if (!filteredDates.length) {
msg = `No better dates in ${consulate}, currently available ${availDates[0]}`;
console.log(msg);
await chrome.storage.local.set({ "__status": msg});
continue;
}
console.log(`Dates worth rescheduling in ${consulate}: ${filteredDates}`);
let chosenDate = filteredDates[0];
await chrome.storage.local.set({ "__status": `Found in ${consulate} better date ${chosenDate}`});
// fill date in reschedule form
await delay(PAGE_WAIT_TIME);
document.getElementById("appointments_consulate_appointment_date").value = chosenDate;
document.getElementById("appointments_consulate_appointment_time").innerHTML = "<option></option>"
let availTimes = await getAvailableTimes(config.consulates[consulate].id, chosenDate);
if (!availTimes) {
msg = `Failed to fetch available timeslots in ${consulate} at ${chosenDate}`;
console.log(msg);
await chrome.storage.local.set({ "__status": msg});
continue;
}
if (availTimes.length == 0) {
msg = `No timeslots in ${consulate} at ${chosenDate}`;
console.log(msg);
await chrome.storage.local.set({ "__status": msg});
continue;
}
console.log(`Available timeslots in ${consulate} at ${chosenDate}: ${availTimes}`);
let chosenTime = availTimes[0];
// fill timeslot in reschedule form
await delay(PAGE_WAIT_TIME);
document.getElementById("appointments_consulate_appointment_time").innerHTML = `<option value='${chosenTime}'>${chosenTime}</option>`;
document.getElementById("appointments_consulate_appointment_time").value = chosenTime;
// TODO process ASC facilities here
await delay(PAGE_WAIT_TIME);
document.getElementById("appointments_submit").removeAttribute("disabled");
document.getElementById("appointments_submit").click();
msg = `Found better appointment in ${consulate} at ${chosenDate} ${chosenTime}`;
console.log(msg);
await sendNotification(msg);
if (!config.consulates[consulate].autobook) {
isFoundAppointment = true;
} else {
await delay(PAGE_WAIT_TIME);
msg = `Auto booking in ${consulate} at ${chosenDate} ${chosenTime}`;
console.log(msg);
await sendNotification(msg);
document.querySelector(".reveal-overlay:last-child [data-reveal] .button.alert").click();
}
} // end consulates loop
for (let consulate in config.consulates) {
if (!prev_config?.consulates?.[consulate]) continue;
if (config.consulates[consulate].nextCheckAt != prev_config.consulates[consulate].nextCheckAt) {
console.log(`Next check for ${consulate} at ${config.consulates[consulate].nextCheckAt}`);
}
}
await chrome.storage.local.set({ "__consulates": config.consulates });
isRunning = false;
return;
}
else if (isConfirmationPage) {
// go back to dashboard after successful reschedule
await delay(PAGE_WAIT_TIME);
config.currentAppt = { consulate: null, date: null};
await chrome.storage.local.set({"__currentAppt": config.currentAppt});
console.log('Rescheduled successfully');
// switch off autobook for all consulates after successful reschedule
for (let consulate in config.consulates) {
config.consulates[consulate].autobook = false;
}
await goToDashboardPage();
}
// console.log('runner done');
isRunning = false;
}
setInterval(runner, 1000);