diff --git a/popup/popup.css b/popup/popup.css index 5a3e88d..51fcbe5 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -8,6 +8,17 @@ opacity: 1; } +#resetStatus { + opacity: 0; + transition: opacity 0.5s; + color: red; +} + +#resetStatus.show { + opacity: 1; +} + + body { min-width: 375px; } diff --git a/popup/popup.html b/popup/popup.html index 5a78501..66423cf 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -82,6 +82,13 @@ Status: Inactive +
+
+ + + Cleaned up! +
+
diff --git a/popup/popup.js b/popup/popup.js index bb704d6..aeb1e3c 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -9,8 +9,9 @@ document.getElementById("password").value = items["__password"] || ""; document.getElementById("frequency").value = items["__frequency"] || 1; document.getElementById("status").innerText = items["__status"] || "unknown"; - document.getElementById("currApptConsulate").innerText = items["__apptConsulate"] || "somewhere"; - document.getElementById("currApptDate").innerText = items["__apptDate"] || "sometime"; + let currentAppt = items["__currentAppt"] || {"consulate": "unknown", "date": "unknown"}; + document.getElementById("currApptConsulate").innerText = currentAppt["consulate"]; + document.getElementById("currApptDate").innerText = currentAppt["date"]; document.getElementById("deltaAppt").value = items["__deltaAppt"] || 1; document.getElementById("deltaNow").value = items["__deltaNow"] || 1; document.getElementById("autobook").checked = items["__autobook"] || false; @@ -80,4 +81,32 @@ chrome.storage.local.set({ __deltaNow: this.value }); }); + // reset button + document.getElementById("resetButton").addEventListener("click", async () => { + if (confirm("Are you sure you want to reset?")) { + await chrome.storage.local.get().then(items => { + chrome.storage.local.clear(); + // keep user parameters + chrome.storage.local.set({ + "__activate": items["__activate"] || false, + "__username": items["__username"] || "", + "__password": items["__password"] || "", + "__frequency": items["__frequency"] || 1, + "__deltaAppt": items["__deltaAppt"] || 1, + "__deltaNow": items["__deltaNow"] || 1, + "__autobook": items["__autobook"] || false, + }); + }); + location.reload(); + } + }); + + // show config button + document.getElementById("showConfigButton").addEventListener("click", async () => { + let config = await chrome.storage.local.get(); + let configStr = JSON.stringify(config, null, 2); + let url = "data:text/plain;charset=utf-8," + encodeURIComponent(configStr); + chrome.tabs.create({ url: url }); +}); + })(); diff --git a/scripts/content.js b/scripts/content.js index 6986f64..97db5a3 100644 --- a/scripts/content.js +++ b/scripts/content.js @@ -4,8 +4,8 @@ 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 MINUTE = 60; +const SOFT_BAN_TIMEOUT = 27 * MINUTE; const NOTIF_CHANNEL = "snegov_test" let config = { @@ -15,8 +15,10 @@ let config = { frequency: null, countdown: null, apptId: null, - apptDate: null, - apptConsulate: null, + currentAppt: { + consulate: null, + date: null, + }, signinAttempts: null, consulates: null, deltaAppt: null, @@ -39,6 +41,17 @@ async function sendNotification(message, channel = NOTIF_CHANNEL) { .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 isSignInPage() { return Boolean(window.location.pathname.match(/^\/\w{2}-\w{2}\/n?iv\/users\/sign_in/)); @@ -107,13 +120,15 @@ async function getAppointmentId() { async function getConsulates() { let consulatesSelect = document.querySelector("#appointments_consulate_appointment_facility_id") let consulatesDict = {}; + for (let option of consulatesSelect.options) { - if (!option.value) continue; + if (!option.value) continue; // skip empty option consulatesDict[option.text] = { "id": parseInt(option.value), "isSelected": option.selected, "bestDate": null, "currentDate": null, + "nextCheckAt": getFutureDate(0, 60), }; } return consulatesDict; @@ -129,6 +144,7 @@ async function getAvailableDates(consulateId) { return dateList; }) .catch(e => null); + // TODO catch for unauthorized return dates; } @@ -148,6 +164,7 @@ async function getAvailableTimes(consulateId, date) { .then(d => d.json()) .then(data => data.available_times) .catch(e => null); + // TODO catch for unauthorized return times; } @@ -168,8 +185,7 @@ async function runner() { config.countdown = result['__countdown'] || 0; config.signinAttempts = result['__signinAttempts'] || 0; config.apptId = result['__apptId'] || null; - config.apptDate = result['__apptDate'] || null; - config.apptConsulate = result['__apptConsulate'] || null; + config.currentAppt = result['__currentAppt'] || { consulate: null, date: null }; config.consulates = result['__consulates'] || null; config.deltaAppt = result['__deltaAppt'] || 1; config.deltaNow = result['__deltaNow'] || 1; @@ -184,14 +200,21 @@ async function runner() { 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]}`); + console.log(`Config change: ${key}, ${JSON.stringify(prev_config[key])} => ${JSON.stringify(config[key])}`); - // reduce countdown if frequency is reduced + // reduce wait times for consulates if frequency is increased 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 }); + 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}`); + } + } + if (wasChanged) { + await chrome.storage.local.set({ "__consulates": config.consulates }); } } @@ -323,23 +346,25 @@ async function runner() { let apptDate = new Date(apptInfo.match(/\d{1,2} \w+, \d{4}/)[0]); apptDate = apptDate.toISOString().slice(0, 10); if (apptDate && apptConsulate - && (apptDate != config.apptDate || apptConsulate != config.apptConsulate)) { - console.log(`New appointment date: ${apptDate} at ${apptConsulate}, old: ${config.apptDate} at ${config.apptConsulate}`); - config.apptDate = apptDate; - config.apptConsulate = apptConsulate; - await chrome.storage.local.set({ "__apptDate": apptDate }); - await chrome.storage.local.set({ "__apptConsulate": 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"); + 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.apptDate) { + if (!config.currentAppt.date) { console.log('No appointment date is set, going back to dashboard'); window.location = window.location.pathname.replace(/schedule.*/g, "/account"); await delay(PAGE_WAIT_TIME); @@ -389,28 +414,30 @@ async function runner() { } for (let consulate of selectedConsulates) { + // skip if not time to check + if (config.consulates[consulate].nextCheckAt > new Date().toISOString()) { + continue; + } + 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 }); + 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 }); - isRunning = false; - return; + continue; } // if empty list, either we're banned or non operational hours or dead consulate + // wait for some time before checking again if (availDates.length == 0) { msg = `No available dates in ${consulate}, probably banned`; console.log(msg); await chrome.storage.local.set({ "__status": msg }); - config.countdown = SOFT_BAN_COUNTDOWN; - await chrome.storage.local.set({ "__countdown": config.countdown }); - isRunning = false; - return; + config.consulates[consulate].nextCheckAt = getFutureDate(SOFT_BAN_TIMEOUT, 10); + continue; } console.log(`Available dates for ${consulate}: ${availDates}`); @@ -420,16 +447,14 @@ async function runner() { console.log(`New record for ${consulate}: ${availDates[0]}`); 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); + 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}); - isRunning = false; - return; + continue; } console.log(`Dates worth rescheduling in ${consulate}: ${filteredDates}`); @@ -446,15 +471,13 @@ async function runner() { msg = `Failed to fetch available timeslots in ${consulate} at ${chosenDate}`; console.log(msg); await chrome.storage.local.set({ "__status": msg}); - isRunning = false; - return; + continue; } if (availTimes.length == 0) { msg = `No timeslots in ${consulate} at ${chosenDate}`; console.log(msg); await chrome.storage.local.set({ "__status": msg}); - isRunning = false; - return; + continue; } console.log(`Available timeslots in ${consulate} at ${chosenDate}: ${availTimes}`); let chosenTime = availTimes[0]; @@ -481,15 +504,21 @@ async function runner() { } } // end consulates loop + for (let consulate in config.consulates) { + 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 schedule after successful reschedule await delay(PAGE_WAIT_TIME); - config.apptDate = null; - await chrome.storage.local.set({"__apptDate": config.apptDate}); - config.apptConsulate = null; - await chrome.storage.local.set({"__apptConsulate": config.apptConsulate}); + config.currentAppt = { consulate: null, date: null}; + await chrome.storage.local.set({"__currentAppt": config.currentAppt}); console.log('Rescheduled successfully'); window.location = window.location.pathname.replace(/schedule.*/g, ""); await delay(PAGE_WAIT_TIME);