diff --git a/manifest.json b/manifest.json index cfad748..b631918 100644 --- a/manifest.json +++ b/manifest.json @@ -6,7 +6,7 @@ "content_scripts": [ { "matches": ["https://ais.usvisa-info.com/*"], - "js": ["scripts/content.js"] + "js": ["scripts/content.js", "scripts/lodash-4.17.15-core.min.js"] } ], "action": { diff --git a/popup/popup.css b/popup/popup.css index bdaad02..352cdcd 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -7,3 +7,7 @@ #saveStatus.show { opacity: 1; } + +body { + min-width: 350px; +} diff --git a/popup/popup.html b/popup/popup.html index 2ce9934..9a2d828 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -6,12 +6,6 @@ not-a-rescheduler popup -
@@ -48,7 +42,7 @@
- +
diff --git a/popup/popup.js b/popup/popup.js index 7352f33..620c650 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -52,6 +52,7 @@ }); saveCredsButton.addEventListener("click", async () => { await save_credentials(); + await chrome.storage.local.set({ "__signinAttempts": 0 }); }); showPasswordButton.addEventListener("mousedown", function() { diff --git a/scripts/content.js b/scripts/content.js index 5e00901..722a49e 100644 --- a/scripts/content.js +++ b/scripts/content.js @@ -4,7 +4,9 @@ async function delay(ms) { const pathnameRegex = /^\/\w{2}-\w{2}\/n?iv/; const MAX_SIGNIN_ATTEMPTS = 1; -const PAGE_WAIT_TIME = 5000; +const PAGE_WAIT_TIME = 3792; +const MINUTE = 67; +const SOFT_BAN_COUNTDOWN = 27 * MINUTE; let config = { activate: null, @@ -15,8 +17,8 @@ let config = { apptId: null, apptDate: null, signinAttempts: null, + consulates: null, }; -let minute = 60; let isRunning = false; @@ -50,16 +52,12 @@ function isNotEnglishPage() { } async function switchToEnglishPage() { - console.log('Changing page to English'); - await chrome.storage.local.set({ "__status": "switching to English" }); 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() { - console.log('Going to sign in page') - await chrome.storage.local.set({ "__status": "going to sign in page" }); document.querySelector(".homeSelectionsContainer a[href*='/sign_in']").click(); await delay(PAGE_WAIT_TIME); return isSignInPage(); @@ -88,11 +86,50 @@ async function getAppointmentId() { return apptId; } -async function checkDates() { - console.log('checkDates start'); - config.countdown = config.frequency * minute; - await chrome.storage.local.set({ "__countdown": config.countdown }); - console.log('checkDates done'); +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() { @@ -108,11 +145,12 @@ async function runner() { config.activate = result['__activate'] || false; config.username = result['__username'] || ""; config.password = result['__password'] || ""; - config.frequency = result['__frequency'] || 1; + 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; if (prev_config.activate === null) { console.log('Reading config: ' + JSON.stringify(config)); @@ -120,15 +158,14 @@ async function runner() { return; } - let configChanged = false; for (let key in config) { - if (config.hasOwnProperty(key) && config[key] !== prev_config[key]) { + if (config.hasOwnProperty(key) + && (!_.isEqual(config[key], prev_config[key]) || prev_config[key] === null)) { console.log(`Config change: ${key}, ${prev_config[key]} => ${config[key]}`); - configChanged = true; // reduce countdown if frequency is reduced if (key === 'frequency') { - let max_countdown = config[key] * minute; + let max_countdown = config[key] * MINUTE; if (config.countdown > max_countdown) { config.countdown = max_countdown; await chrome.storage.local.set({ "__countdown": config.countdown }); @@ -157,10 +194,6 @@ async function runner() { } } } - if (configChanged) { - // print whole config - console.log(JSON.stringify(config)); - } if (!config.activate) { isRunning = false; @@ -185,7 +218,7 @@ async function runner() { config.countdown -= 1; console.log(`Countdown: ${config.countdown}`); await chrome.storage.local.set({ "__countdown": config.countdown }); - await chrome.storage.local.set({ "__status": `waiting, ${config.countdown}s` }); + // await chrome.storage.local.set({ "__status": `waiting, ${config.countdown}s` }); isRunning = false; return; } @@ -202,7 +235,9 @@ async function runner() { return; }; - } else if (isSignInPage()) { + } + + else if (isSignInPage()) { // Prevent brute forcing if (config.signinAttempts >= MAX_SIGNIN_ATTEMPTS) { await chrome.storage.local.set({ "__status": "too many sign in attempts" }); @@ -223,7 +258,9 @@ async function runner() { return; }; - } else if (isDashboardPage()) { + } + + else if (isDashboardPage()) { // reset signin attempts when successfully logged in config.signinAttempts = 0; await chrome.storage.local.set({ "__status": "fetching appointment info" }); @@ -263,6 +300,119 @@ async function runner() { 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); + 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, 30, 3); + 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(); + } + + } // console.log('runner done'); isRunning = false; diff --git a/scripts/lodash-4.17.15-core.min.js b/scripts/lodash-4.17.15-core.min.js new file mode 100644 index 0000000..bb543ff --- /dev/null +++ b/scripts/lodash-4.17.15-core.min.js @@ -0,0 +1,29 @@ +/** + * @license + * Lodash (Custom Build) lodash.com/license | Underscore.js 1.8.3 underscorejs.org/LICENSE + * Build: `lodash core -o ./dist/lodash.core.js` + */ +;(function(){function n(n){return H(n)&&pn.call(n,"callee")&&!yn.call(n,"callee")}function t(n,t){return n.push.apply(n,t),n}function r(n){return function(t){return null==t?Z:t[n]}}function e(n,t,r,e,u){return u(n,function(n,u,o){r=e?(e=false,n):t(r,n,u,o)}),r}function u(n,t){return j(t,function(t){return n[t]})}function o(n){return n instanceof i?n:new i(n)}function i(n,t){this.__wrapped__=n,this.__actions__=[],this.__chain__=!!t}function c(n,t,r){if(typeof n!="function")throw new TypeError("Expected a function"); +return setTimeout(function(){n.apply(Z,r)},t)}function f(n,t){var r=true;return mn(n,function(n,e,u){return r=!!t(n,e,u)}),r}function a(n,t,r){for(var e=-1,u=n.length;++et}function b(n,t,r,e,u){return n===t||(null==n||null==t||!H(n)&&!H(t)?n!==n&&t!==t:y(n,t,r,e,b,u))}function y(n,t,r,e,u,o){var i=Nn(n),c=Nn(t),f=i?"[object Array]":hn.call(n),a=c?"[object Array]":hn.call(t),f="[object Arguments]"==f?"[object Object]":f,a="[object Arguments]"==a?"[object Object]":a,l="[object Object]"==f,c="[object Object]"==a,a=f==a;o||(o=[]);var p=An(o,function(t){return t[0]==n}),s=An(o,function(n){ +return n[0]==t});if(p&&s)return p[1]==t;if(o.push([n,t]),o.push([t,n]),a&&!l){if(i)r=T(n,t,r,e,u,o);else n:{switch(f){case"[object Boolean]":case"[object Date]":case"[object Number]":r=J(+n,+t);break n;case"[object Error]":r=n.name==t.name&&n.message==t.message;break n;case"[object RegExp]":case"[object String]":r=n==t+"";break n}r=false}return o.pop(),r}return 1&r||(i=l&&pn.call(n,"__wrapped__"),f=c&&pn.call(t,"__wrapped__"),!i&&!f)?!!a&&(r=B(n,t,r,e,u,o),o.pop(),r):(i=i?n.value():n,f=f?t.value():t, +r=u(i,f,r,e,o),o.pop(),r)}function g(n){return typeof n=="function"?n:null==n?X:(typeof n=="object"?d:r)(n)}function _(n,t){return nt&&(t=-t>u?0:u+t),r=r>u?u:r,0>r&&(r+=u),u=t>r?0:r-t>>>0,t>>>=0,r=Array(u);++ei))return false;for(var c=-1,f=true,a=2&r?[]:Z;++cr?jn(e+r,0):r:0,r=(r||0)-1;for(var u=t===t;++rarguments.length,mn)}function G(n,t){var r;if(typeof t!="function")throw new TypeError("Expected a function");return n=Fn(n), +function(){return 0<--n&&(r=t.apply(this,arguments)),1>=n&&(t=Z),r}}function J(n,t){return n===t||n!==n&&t!==t}function M(n){var t;return(t=null!=n)&&(t=n.length,t=typeof t=="number"&&-1=t),t&&!U(n)}function U(n){return!!V(n)&&(n=hn.call(n),"[object Function]"==n||"[object GeneratorFunction]"==n||"[object AsyncFunction]"==n||"[object Proxy]"==n)}function V(n){var t=typeof n;return null!=n&&("object"==t||"function"==t)}function H(n){return null!=n&&typeof n=="object"}function K(n){ +return typeof n=="number"||H(n)&&"[object Number]"==hn.call(n)}function L(n){return typeof n=="string"||!Nn(n)&&H(n)&&"[object String]"==hn.call(n)}function Q(n){return typeof n=="string"?n:null==n?"":n+""}function W(n){return null==n?[]:u(n,Dn(n))}function X(n){return n}function Y(n,r,e){var u=Dn(r),o=h(r,u);null!=e||V(r)&&(o.length||!u.length)||(e=r,r=n,n=this,o=h(r,Dn(r)));var i=!(V(e)&&"chain"in e&&!e.chain),c=U(n);return mn(o,function(e){var u=r[e];n[e]=u,c&&(n.prototype[e]=function(){var r=this.__chain__; +if(i||r){var e=n(this.__wrapped__);return(e.__actions__=A(this.__actions__)).push({func:u,args:arguments,thisArg:n}),e.__chain__=r,e}return u.apply(n,t([this.value()],arguments))})}),n}var Z,nn=1/0,tn=/[&<>"']/g,rn=RegExp(tn.source),en=/^(?:0|[1-9]\d*)$/,un=typeof self=="object"&&self&&self.Object===Object&&self,on=typeof global=="object"&&global&&global.Object===Object&&global||un||Function("return this")(),cn=(un=typeof exports=="object"&&exports&&!exports.nodeType&&exports)&&typeof module=="object"&&module&&!module.nodeType&&module,fn=function(n){ +return function(t){return null==n?Z:n[t]}}({"&":"&","<":"<",">":">",'"':""","'":"'"}),an=Array.prototype,ln=Object.prototype,pn=ln.hasOwnProperty,sn=0,hn=ln.toString,vn=on._,bn=Object.create,yn=ln.propertyIsEnumerable,gn=on.isFinite,_n=function(n,t){return function(r){return n(t(r))}}(Object.keys,Object),jn=Math.max,dn=function(){function n(){}return function(t){return V(t)?bn?bn(t):(n.prototype=t,t=new n,n.prototype=Z,t):{}}}();i.prototype=dn(o.prototype),i.prototype.constructor=i; +var mn=function(n,t){return function(r,e){if(null==r)return r;if(!M(r))return n(r,e);for(var u=r.length,o=t?u:-1,i=Object(r);(t?o--:++or&&(r=jn(e+r,0));n:{for(t=g(t),e=n.length,r+=-1;++re||o&&c&&a||!u&&a||!i){r=1;break n}if(!o&&r