// 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 = 67; const SOFT_BAN_COUNTDOWN = 27 * MINUTE; const NOTIF_CHANNEL = "snegov_test" let config = { activate: null, username: null, password: null, frequency: null, countdown: null, apptId: null, apptDate: null, signinAttempts: null, consulates: null, deltaAppt: null, deltaNow: null, }; let isRunning = 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: message, }) .then(() => console.log('Notification sent')) .catch(e => console.error(e)); } 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 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 goToSignInPage() { document.querySelector(".homeSelectionsContainer a[href*='/sign_in']").click(); await delay(PAGE_WAIT_TIME); return isSignInPage(); } 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; consulatesDict[option.text] = { "id": parseInt(option.value), "isSelected": option.selected, "bestDate": null, }; } 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(e => null); 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 => null); 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.countdown = result['__countdown'] || 0; config.signinAttempts = result['__signinAttempts'] || 0; config.apptId = result['__apptId'] || null; config.apptDate = result['__apptDate'] || 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(config)); isRunning = false; return; } for (let key in config) { if (config.hasOwnProperty(key) && !_.isEqual(config[key], prev_config[key])) { console.log(`Config change: ${key}, ${prev_config[key]} => ${config[key]}`); // reduce countdown if frequency is reduced if (key === 'frequency') { let max_countdown = config[key] * MINUTE; if (config.countdown > max_countdown) { config.countdown = max_countdown; await chrome.storage.local.set({ "__countdown": config.countdown }); } } if (key === 'activate') { if (config[key]) { console.log('Activating extension'); config.countdown = 5; await chrome.storage.local.set({ "__countdown": config.countdown }); } else { console.log('Deactivating extension'); // reset countdown when deactivating the extension config.countdown = 0; await chrome.storage.local.set({ "__countdown": config.countdown }); 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; } if (config.countdown > 0) { config.countdown -= 1; if (config.countdown % 30 === 0 || config.countdown <= 5) { console.log(`Countdown: ${config.countdown}s`); } await chrome.storage.local.set({ "__countdown": config.countdown }); // await chrome.storage.local.set({ "__status": `waiting, ${config.countdown}s` }); isRunning = false; return; } if (isNotEnglishPage()) { await switchToEnglishPage(); } if (isLoggedOutPage()) { if (!await goToSignInPage()) { console.log('Failed to go to sign in page'); await chrome.storage.local.set({ "__status": "failed to go to sign in page" }); 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 console.log('Signing in attempt: ' + config.signinAttempts) await chrome.storage.local.set({ "__status": "signing in #" + config.signinAttempts }); let signedIn = await enterCredentials(); config.signinAttempts += 1; await chrome.storage.local.set({ "__signinAttempts": config.signinAttempts }); if (!signedIn) { console.log('Sign in failed'); await chrome.storage.local.set({ "__status": "sign in failed" }); 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: ${apptId}`); await chrome.storage.local.set({ "__apptId": apptId }); } else { console.log('No appointments found'); await chrome.storage.local.set({ "__status": "no appointment found" }); isRunning = false; return; } } // get current appointment date let apptInfo = document.querySelector("p.consular-appt [href*='" + config.apptId + "']").parentNode.parentNode.parentNode if (!apptInfo.querySelector("h4").innerText.match(/Attend Appointment/)) { console.log('Appointment not available'); await chrome.storage.local.set({ "__status": "appointment not available" }); isRunning = false; return; } let apptDate = new Date(apptInfo.querySelector("p.consular-appt").innerText.match(/\d{1,2} \w+, \d{4}/)[0]); apptDate = apptDate.toISOString().slice(0, 10); if (config.apptDate && apptDate != config.apptDate) { config.apptDate = apptDate; await chrome.storage.local.set({ "__apptDate": apptDate }); } // go to appointment page let apptLink = apptInfo.querySelector("p.consular-appt [href]").getAttribute("href").replace("/addresses/consulate", "/appointment"); window.location.href = apptLink; await delay(PAGE_WAIT_TIME); } else if (isAppointmentPage()) { // 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")) { console.log('No available appointments'); await chrome.storage.local.set({ "__status": "no available appointments" }); isRunning = false; return; }; 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) { console.log('No selected consulates found'); await chrome.storage.local.set({ "__status": "no selected consulates" }); isRunning = false; return; } for (let consulate of selectedConsulates) { console.log('Checking dates for ' + consulate); let availDates = await getAvailableDates(config.consulates[consulate].id); config.countdown = config.frequency * MINUTE; await chrome.storage.local.set({ "__countdown": config.countdown }); if (!availDates) { console.log('Failed to fetch available dates in ' + consulate); await chrome.storage.local.set({ "__status": "no dates in " + consulate }); isRunning = false; return; } // if empty list, either we're banned or non operational hours or dead consulate if (availDates.length == 0) { console.log(`No available dates in ${consulate}, probably banned`); await chrome.storage.local.set({ "__status": "no dates in " + consulate }); config.countdown = SOFT_BAN_COUNTDOWN; await chrome.storage.local.set({ "__countdown": config.countdown }); isRunning = false; return; } console.log(`Available dates for ${consulate}: ${availDates}`); config.consulates[consulate].bestDate = availDates[0]; await chrome.storage.local.set({ "__consulates": config.consulates }); // filter dates with our requests let filteredDates = await filterDates(availDates, config.apptDate, config.deltaAppt, config.deltaNow); if (!filteredDates.length) { console.log('Nothing interesting found in ' + consulate); await chrome.storage.local.set({ "__status": `Nothing in ${consulate}, best date ${availDates[0]}`}); isRunning = false; return; } 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 = "" let availTimes = await getAvailableTimes(config.consulates[consulate].id, chosenDate); if (!availTimes) { console.log(`Failed to fetch available timeslots in ${consulate} at ${chosenDate}`); await chrome.storage.local.set({ "__status": `failed to fetch timeslots in ${consulate} at ${chosenDate}`}); isRunning = false; return; } if (availTimes.length == 0) { console.log(`No timeslots in ${consulate} at ${chosenDate}`); await chrome.storage.local.set({ "__status": `no timeslots in ${consulate} at ${chosenDate}`}); isRunning = false; return; } 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 = ``; 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(); await sendNotification(`Found better appointment in ${consulate} at ${chosenDate} ${chosenTime}`); } } else if (isConfirmationPage) { // go back to schedule after successful reschedule await delay(PAGE_WAIT_TIME); window.location = page.replace(/schedule.*/g, ""); } // console.log('runner done'); isRunning = false; } setInterval(runner, 1000);