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;++e
t}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