Compare commits
71 Commits
ais-visa-a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 3478fd5a6e | |||
| 8697c717b8 | |||
| c31ab1eb64 | |||
| cba2d21c7e | |||
| e50e3c43f6 | |||
| 5ddffe06e7 | |||
| 7331c266f4 | |||
| 18c8b2112c | |||
| 2aaaa96dcf | |||
| 425ff34ea0 | |||
| 3c72a20878 | |||
| 4f83907506 | |||
| 5e96252bb4 | |||
| 17bc09d67e | |||
| 6af70c07bc | |||
| ce539b61a1 | |||
| ca84e18468 | |||
| f66c6ecbf9 | |||
| f580244a5d | |||
| 90a58961d4 | |||
| 3f2ba8bd2a | |||
| d4c8835e9b | |||
| 776b0afdab | |||
| c428e4ab11 | |||
| d19b9ced8f | |||
| 4a506de6fe | |||
| 2070cdb30a | |||
| fcb9f9dbf3 | |||
| 094e2d2b81 | |||
| c4307472ca | |||
| 616367a796 | |||
| 66e0ac47ba | |||
| 86c7576131 | |||
| bcdcd0001a | |||
| d075d8ac3e | |||
| 15613c19fd | |||
| 84d608a9f4 | |||
| 9bbcc6e4a2 | |||
| b12243afaf | |||
| 923dd7495b | |||
| 20b3a15a99 | |||
| 33f11a6528 | |||
| 32cf1a5acf | |||
| 91672725fa | |||
| 99e61bb1fc | |||
| b0b1b0f9c4 | |||
| 984f3e4ceb | |||
| 30438a68e8 | |||
| c5582e7662 | |||
| ccfd6c1b9b | |||
| 1088d46fac | |||
| 1bdabb88c2 | |||
| c86eb8470f | |||
| b693401ff4 | |||
| 92d9f51ab1 | |||
| 363e842b56 | |||
| 9b820859c7 | |||
| 968e764020 | |||
| 2d3e1b01be | |||
| c8e0028cb9 | |||
| 9f5fa888ef | |||
| bdf4b5a7c4 | |||
| d5475e7055 | |||
| 3f61d82ec5 | |||
| b8ab66cd78 | |||
| 54ed7c302e | |||
| cb9b2bf1e8 | |||
| 3a87f9946c | |||
| 466c0b6b17 | |||
| 762c5fa196 | |||
| b5b6ca363d |
45
content.js
45
content.js
@ -1,45 +0,0 @@
|
||||
// Function to send POST request
|
||||
function sendPostRequest(data) {
|
||||
fetch('https://ntfy.sh/snegov', {
|
||||
method: 'POST', // PUT works too
|
||||
body: `US visa: ${data}`
|
||||
})
|
||||
.then(response => {
|
||||
console.log('POST request sent successfully:', data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error sending POST request:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkDate() {
|
||||
const targetElement = document.querySelector('.swal2-html-container');
|
||||
// Get current time
|
||||
const currentTime = new Date();
|
||||
const formattedTime = currentTime.toISOString();
|
||||
|
||||
if (targetElement) {
|
||||
const availabilitySpan = targetElement.querySelector('span[style="color: lightgreen;"]');
|
||||
const appointmentSpan = targetElement.querySelector('span[style="color: orange"]');
|
||||
|
||||
const date_avail = availabilitySpan ? availabilitySpan.textContent.match(/Latest availability: (.*)\./)[1].trim() : null;
|
||||
const date_booked = appointmentSpan ? appointmentSpan.textContent.match(/Your current appointment is on (.*)/)[1].trim() : null;
|
||||
|
||||
if (date_avail && date_booked) {
|
||||
const date_avail_date = new Date(date_avail);
|
||||
const date_booked_date = new Date(date_booked);
|
||||
console.log(`${formattedTime}: available date ${date_avail}; booked on ${date_booked}`);
|
||||
|
||||
// Compare the dates
|
||||
if (date_avail_date < date_booked_date) {
|
||||
const message = `available date ${date_avail}; booked on ${date_booked}`;
|
||||
sendPostRequest(message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('Element with class "swal2-html-container" not found');
|
||||
}
|
||||
}
|
||||
|
||||
// Set an interval to check the date every 10 seconds
|
||||
setInterval(checkDate, 20000);
|
||||
BIN
images/icon_passport_128.png
Normal file
BIN
images/icon_passport_128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
images/icon_passport_16.png
Normal file
BIN
images/icon_passport_16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 309 B |
BIN
images/icon_passport_48.png
Normal file
BIN
images/icon_passport_48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
images/icon_passport_512.png
Normal file
BIN
images/icon_passport_512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@ -1,12 +1,22 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Page Change Detector",
|
||||
"version": "1.0",
|
||||
"permissions": ["activeTab"],
|
||||
"name": "not-a-rescheduler",
|
||||
"version": "0.0.10",
|
||||
"permissions": [ "storage", "tabs", "activeTab", "notifications", "declarativeContent" ],
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://ais.usvisa-info.com/*"],
|
||||
"js": ["content.js"]
|
||||
"js": ["scripts/content.js", "scripts/lodash-core.min.js"]
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"default_popup": "popup/popup.html",
|
||||
"default_icon": "images/icon_passport_48.png"
|
||||
},
|
||||
"icons": {
|
||||
"16": "images/icon_passport_16.png",
|
||||
"48": "images/icon_passport_48.png",
|
||||
"128": "images/icon_passport_128.png",
|
||||
"512": "images/icon_passport_512.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
7
popup/bootstrap.bundle.min.js
vendored
Normal file
7
popup/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
popup/bootstrap.bundle.min.js.map
Normal file
1
popup/bootstrap.bundle.min.js.map
Normal file
File diff suppressed because one or more lines are too long
6
popup/bootstrap.min.css
vendored
Normal file
6
popup/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
32
popup/popup.css
Normal file
32
popup/popup.css
Normal file
@ -0,0 +1,32 @@
|
||||
#saveStatus {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
color: green;
|
||||
}
|
||||
|
||||
#saveStatus.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#resetStatus {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
color: red;
|
||||
}
|
||||
|
||||
#resetStatus.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.smooth-text {
|
||||
transition: opacity 0.5s;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.smooth-text.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 375px;
|
||||
}
|
||||
106
popup/popup.html
Normal file
106
popup/popup.html
Normal file
@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>not-a-rescheduler popup</title>
|
||||
<link rel="stylesheet" href="bootstrap.min.css">
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- version info -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h3 style="white-space: nowrap;">not-a-rescheduler <span id="version"></span></h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- activate slider -->
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="form-check form-switch" style="text-align: left;">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="activate">
|
||||
<label class="form-check-label" for="activate">Activate the script</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- credentials -->
|
||||
<div class="row">
|
||||
<div class="col form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" class="form-control" id="username" placeholder="Username">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" id="password" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<button type="button" id="showPassword" class="btn btn-link btn-sm">Show</button>
|
||||
</div>
|
||||
<div class="col">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="saveButton">Save credentials</button>
|
||||
<span id="saveStatus">Saved!</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- reschedule parameters -->
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="frequency">Check frequency<br>(every <span id="frequency_info">N</span> minutes)</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="range" id="frequency" name="frequency" min="0.5" max="10" step="0.5">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="deltaAppt">Appointment delta (days)</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="number" class="form-control" id="deltaAppt" placeholder="days" min="1" max="365">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="deltaNow">Preparation time (days)</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="number" class="form-control" id="deltaNow" placeholder="days" min="1" max="365">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- current appointment -->
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Current appointment: <span id="currApptConsulate">somewhere</span>, <span id="currApptDate">some time</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Status: <span class="smooth-text" id="status">Inactive</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- consulates -->
|
||||
<div id="consulatesConfig">
|
||||
</div>
|
||||
|
||||
<!-- buttons -->
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<button type="button" class="btn btn-info btn-sm" id="showConfigButton">Config</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" id="resetButton">Reset</button>
|
||||
<span id="resetStatus">Cleaned up!</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="bootstrap.bundle.min.js"></script>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
205
popup/popup.js
Normal file
205
popup/popup.js
Normal file
@ -0,0 +1,205 @@
|
||||
function smoothTextChange(element, newText) {
|
||||
// Temporarily hide the element
|
||||
element.classList.add('hide');
|
||||
|
||||
setTimeout(() => {
|
||||
element.innerText = newText;
|
||||
element.classList.remove('hide');
|
||||
}, 500); // Wait for the transition to finish
|
||||
}
|
||||
|
||||
(async function() {
|
||||
|
||||
const $version = await new Promise(r => chrome.management.getSelf(self => r(self.version)));
|
||||
document.getElementById("version").innerText = `v${$version}`;
|
||||
|
||||
await chrome.storage.local.get().then(items => {
|
||||
document.getElementById("activate").checked = items["cfg_activate"] || false;
|
||||
document.getElementById("username").value = items["cfg_username"] || "";
|
||||
document.getElementById("password").value = items["cfg_password"] || "";
|
||||
document.getElementById("frequency").value = items["cfg_frequency"] || 1;
|
||||
document.getElementById("frequency_info").innerText = items["cfg_frequency"] || 1;
|
||||
document.getElementById("status").innerText = items["ctx_statusMsg"] || "unknown";
|
||||
let currentAppt = items["ctx_currentAppt"] || {"consulate": "somewhere", "date": "some time"};
|
||||
document.getElementById("currApptConsulate").innerText = currentAppt["consulate"];
|
||||
document.getElementById("currApptDate").innerText = currentAppt["date"];
|
||||
document.getElementById("deltaAppt").value = items["cfg_deltaAppt"] || 1;
|
||||
document.getElementById("deltaNow").value = items["cfg_deltaNow"] || 1;
|
||||
});
|
||||
|
||||
// update frequency value
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (changes.cfg_frequency)
|
||||
document.getElementById("frequency_info").innerText = changes.cfg_frequency.newValue;
|
||||
});
|
||||
// update status
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (changes.ctx_statusMsg)
|
||||
smoothTextChange(document.getElementById("status"), changes.ctx_statusMsg.newValue);
|
||||
});
|
||||
|
||||
// activate checkbox
|
||||
document.getElementById("activate").addEventListener("change", async e => {
|
||||
await chrome.storage.local.set({ "cfg_activate": e.target.checked });
|
||||
});
|
||||
|
||||
// credentials
|
||||
let usernameField = document.getElementById("username");
|
||||
let passwordField = document.getElementById("password");
|
||||
let showPasswordButton = document.getElementById("showPassword");
|
||||
let saveCredsButton = document.getElementById("saveButton");
|
||||
let saveStatusElement = document.getElementById("saveStatus");
|
||||
|
||||
async function save_credentials() {
|
||||
await chrome.storage.local.set({
|
||||
"cfg_username": usernameField.value,
|
||||
"cfg_password": passwordField.value
|
||||
});
|
||||
saveStatusElement.classList.add("show");
|
||||
setTimeout(() => {
|
||||
saveStatusElement.classList.remove("show");
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
usernameField.addEventListener("keypress", async e => {
|
||||
if (e.key === "Enter") {
|
||||
await save_credentials();
|
||||
}
|
||||
});
|
||||
passwordField.addEventListener("keypress", async e => {
|
||||
if (e.key === "Enter") {
|
||||
await save_credentials();
|
||||
}
|
||||
});
|
||||
saveCredsButton.addEventListener("click", async () => {
|
||||
await save_credentials();
|
||||
await chrome.storage.local.set({ "ctx_signinAttempts": 0 });
|
||||
});
|
||||
|
||||
showPasswordButton.addEventListener("mousedown", function() {
|
||||
passwordField.type = "text";
|
||||
});
|
||||
showPasswordButton.addEventListener("mouseup", function() {
|
||||
passwordField.type = "password";
|
||||
});
|
||||
|
||||
// range sliders
|
||||
document.getElementById("frequency").addEventListener("input", function() {
|
||||
chrome.storage.local.set({ cfg_frequency: this.value });
|
||||
});
|
||||
document.getElementById("deltaAppt").addEventListener("input", function() {
|
||||
chrome.storage.local.set({ cfg_deltaAppt: this.value });
|
||||
});
|
||||
document.getElementById("deltaNow").addEventListener("change", function() {
|
||||
chrome.storage.local.set({ cfg_deltaNow: this.value });
|
||||
});
|
||||
|
||||
// consulates
|
||||
await chrome.storage.local.get(['cfg_consulates', 'ctx_consulates']).then((result) => {
|
||||
let consCfg = result['cfg_consulates'];
|
||||
let consCtx = result['ctx_consulates'];
|
||||
|
||||
let html = `
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
<th scope="col">City</th>
|
||||
<th scope="col" style="white-space: nowrap;">Current Date</th>
|
||||
<th scope="col" style="white-space: nowrap;">Best Date</th>
|
||||
<th scope="col">Book</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`
|
||||
for (let c in consCtx) {
|
||||
let cSelected = consCfg[c]?.isSelected === true ? "checked" : "";
|
||||
let cAutoBook = consCfg[c]?.autoBook === true ? "checked" : "";
|
||||
let cId = consCtx[c].id;
|
||||
html += `
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check form-checkbox" style="text-align: left;">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="isSelected-${cId}" ${cSelected}>
|
||||
</div>
|
||||
</td>
|
||||
<td style="white-space: nowrap;">${c}</td>
|
||||
<td style="white-space: nowrap;"><span class="smooth-text" id="currentDate-${cId}">${consCtx[c].currentDate || "-"}</span></td>
|
||||
<td style="white-space: nowrap;"><span class="smooth-text" id="bestDate-${cId}">${consCtx[c].bestDate || "-"}</span></td>
|
||||
<td>
|
||||
<div class="form-check form-switch" style="text-align: left;">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="autoBook-${cId}" ${cAutoBook}>
|
||||
<!--<label class="form-check-label" for="autoBook-${cId}">Autobook</label>-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
html += `</tbody></table>`;
|
||||
document.getElementById('consulatesConfig').innerHTML = html;
|
||||
|
||||
for (let c in consCtx) {
|
||||
let cId = consCtx[c].id;
|
||||
document.getElementById(`isSelected-${cId}`).addEventListener("change", async e => {
|
||||
consCfg[c].isSelected = e.target.checked;
|
||||
await chrome.storage.local.set({ "cfg_consulates": consCfg });
|
||||
});
|
||||
document.getElementById(`autoBook-${cId}`).addEventListener("change", async e => {
|
||||
consCfg[c].autoBook = e.target.checked;
|
||||
await chrome.storage.local.set({ "cfg_consulates": consCfg });
|
||||
});
|
||||
|
||||
// update current & best dates
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (changes.ctx_consulates) {
|
||||
let newCurrentDate = changes.ctx_consulates.newValue[c].currentDate || "-";
|
||||
let newBestDate = changes.ctx_consulates.newValue[c].bestDate || "-";
|
||||
|
||||
let el = document.getElementById(`currentDate-${cId}`);
|
||||
if (el && el.innerText != newCurrentDate)
|
||||
smoothTextChange(el, newCurrentDate);
|
||||
el = document.getElementById(`bestDate-${cId}`);
|
||||
if (el && el.innerText != newBestDate)
|
||||
smoothTextChange(el, newBestDate);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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({
|
||||
"cfg_activate": items["cfg_activate"] || false,
|
||||
"cfg_username": items["cfg_username"] || "",
|
||||
"cfg_password": items["cfg_password"] || "",
|
||||
"cfg_frequency": items["cfg_frequency"] || 1,
|
||||
"cfg_deltaAppt": items["cfg_deltaAppt"] || 1,
|
||||
"cfg_deltaNow": items["cfg_deltaNow"] || 1,
|
||||
});
|
||||
});
|
||||
document.getElementById("status").innerText = "unknown";
|
||||
document.getElementById("currApptConsulate").innerText = "somewhere";
|
||||
document.getElementById("currApptDate").innerText = "some time";
|
||||
document.getElementById('consulatesConfig').innerHTML = "No consulates found.";
|
||||
|
||||
document.getElementById("resetStatus").classList.add("show");
|
||||
setTimeout(() => {
|
||||
document.getElementById("resetStatus").classList.remove("show");
|
||||
}, 2000);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// 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 });
|
||||
});
|
||||
|
||||
})();
|
||||
744
scripts/content.js
Normal file
744
scripts/content.js
Normal file
@ -0,0 +1,744 @@
|
||||
// Copyright (c) 2024, snegov
|
||||
// All rights reserved.
|
||||
|
||||
const pathnameRegex = /^\/\w{2}-\w{2}\/n?iv/;
|
||||
const MAX_SIGNIN_ATTEMPTS = 3;
|
||||
const PAGE_WAIT_TIME = 3792;
|
||||
const MINUTE = 60;
|
||||
const RND_VAR_COEFF = 0.1;
|
||||
const SOFT_BAN_TIMEOUT = 27;
|
||||
const NOTIF_CHANNEL = "snegov";
|
||||
const CHECK_OPER_HOURS = true;
|
||||
const OPER_HOURS_START = 23;
|
||||
const OPER_HOURS_END = 9;
|
||||
|
||||
let cfg = {
|
||||
activate: undefined,
|
||||
username: undefined,
|
||||
password: undefined,
|
||||
frequency: undefined,
|
||||
consulates: undefined,
|
||||
deltaAppt: undefined,
|
||||
deltaNow: undefined,
|
||||
};
|
||||
let ctx = {
|
||||
apptId: undefined,
|
||||
currentAppt: {
|
||||
consulate: undefined,
|
||||
date: undefined,
|
||||
},
|
||||
signinAttempts: undefined,
|
||||
consulates: undefined,
|
||||
statusMsg: undefined,
|
||||
}
|
||||
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) {
|
||||
const msg = "[US visa bot] " + message
|
||||
await fetch('https://ntfy.sh/' + channel, {
|
||||
method: 'POST',
|
||||
body: msg,
|
||||
})
|
||||
.then(() => console.log('Notification sent: ' + msg))
|
||||
.catch(e => console.error(e));
|
||||
}
|
||||
|
||||
function getRandomInt(max) {
|
||||
return Math.floor(Math.random() * Math.floor(max));
|
||||
}
|
||||
|
||||
function getFutureDate(minutes, frequency) {
|
||||
// return date some amount of minutes in future plus random amount of seconds
|
||||
let futureDate = new Date();
|
||||
const randomVariation = frequency * MINUTE * RND_VAR_COEFF;
|
||||
const randomSign = Math.random() < 0.5 ? -1 : 1;
|
||||
const offset = randomSign * getRandomInt(randomVariation);
|
||||
|
||||
futureDate.setSeconds(futureDate.getSeconds() + minutes * MINUTE + offset);
|
||||
return futureDate.toISOString();
|
||||
}
|
||||
|
||||
function isNoBanTime(frequency = 5) {
|
||||
let now = new Date();
|
||||
return (now.getHours() === OPER_HOURS_START && now.getMinutes() < frequency);
|
||||
}
|
||||
|
||||
function hiddenPassword(cfg) {
|
||||
return {
|
||||
...cfg,
|
||||
password: cfg.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 hasAllKeys(obj, keys) {
|
||||
return keys.every(k => obj.hasOwnProperty(k));
|
||||
}
|
||||
|
||||
function ensureRequiredConsulateProperties(consCtx, consCfg, frequency = 1) {
|
||||
let reqCfgKeys = ["isSelected", "autoBook"];
|
||||
let reqStateKeys = ["id", "bestDate", "currentDate", "nextCheckAt"];
|
||||
|
||||
for (let c in consCtx) {
|
||||
if (!hasAllKeys(consCtx[c], reqStateKeys)) {
|
||||
consCtx[c] = {
|
||||
"id": consCtx[c]?.id || null,
|
||||
"bestDate": consCtx[c]?.bestDate || null,
|
||||
"currentDate": consCtx[c]?.currentDate || null,
|
||||
"nextCheckAt": consCtx[c]?.nextCheckAt || getFutureDate(0, frequency),
|
||||
};
|
||||
}
|
||||
if (!consCfg[c] || !hasAllKeys(consCfg[c], reqCfgKeys)) {
|
||||
consCfg[c] = {
|
||||
"isSelected": consCfg[c]?.isSelected || false,
|
||||
"autoBook": consCfg[c]?.autoBook || false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return [consCtx, consCfg];
|
||||
}
|
||||
|
||||
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 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(username, password) {
|
||||
// close warning modal
|
||||
let modalElement = Array.from(document.querySelectorAll('div'))
|
||||
.find(d => d.textContent.includes('You need to sign in or sign up before continuing.'));
|
||||
if (modalElement && window.getComputedStyle(modalElement).display !== 'none') {
|
||||
let buttons = Array.from(modalElement.querySelectorAll('button'));
|
||||
let okButton = buttons.find(b => b.textContent === 'OK');
|
||||
if (okButton) {
|
||||
okButton.click();
|
||||
await delay(PAGE_WAIT_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("user_email").value = username;
|
||||
document.getElementById("user_password").value = 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),
|
||||
};
|
||||
}
|
||||
return consulatesDict;
|
||||
}
|
||||
|
||||
async function getAvailableDates(consulateId) {
|
||||
const datesUri = window.location.pathname + `/days/${consulateId}.json?appointments[expedite]=false`
|
||||
try {
|
||||
const response = await fetch(datesUri, { headers: {
|
||||
"x-requested-with": "XMLHttpRequest",
|
||||
"accept": "application/json, text/javascript, */*; q=0.01",
|
||||
"cache-control": "no-cache",
|
||||
}});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
console.log('Logged out due to 401 error');
|
||||
await logOut();
|
||||
}
|
||||
const body = await response.text();
|
||||
throw new Error(`HTTP error ${response.status}: ${body}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const dateList = data.map(item => item.date);
|
||||
dateList.sort();
|
||||
return dateList;
|
||||
} catch (e) {
|
||||
console.error('Error:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function filterDates(dates, currentAppt, deltaFromAppt, deltaFromNow) {
|
||||
let maxDate = new Date(currentAppt);
|
||||
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 = 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;
|
||||
|
||||
let prev_cfg = Object.assign({}, cfg);
|
||||
let prev_ctx = Object.assign({}, ctx);
|
||||
|
||||
let result = await new Promise(resolve => chrome.storage.local.get(null, resolve));
|
||||
cfg.activate = result['cfg_activate'] || false;
|
||||
cfg.username = result['cfg_username'] || "";
|
||||
cfg.password = result['cfg_password'] || "";
|
||||
cfg.frequency = parseFloat(result['cfg_frequency'] || 1);
|
||||
cfg.consulates = result['cfg_consulates'] || {};
|
||||
cfg.deltaAppt = result['cfg_deltaAppt'] || 1;
|
||||
cfg.deltaNow = result['cfg_deltaNow'] || 1;
|
||||
|
||||
ctx.apptId = result['ctx_apptId'] || null;
|
||||
ctx.signinAttempts = result['ctx_signinAttempts'] || 0;
|
||||
ctx.currentAppt = result['ctx_currentAppt'] || { consulate: null, date: null };
|
||||
ctx.consulates = result['ctx_consulates'] || {};
|
||||
ctx.statusMsg = result['ctx_statusMsg'] || "";
|
||||
|
||||
[ctx.consulates, cfg.consulates] = ensureRequiredConsulateProperties(
|
||||
ctx.consulates, cfg.consulates, cfg.frequency
|
||||
);
|
||||
await chrome.storage.local.set({ "cfg_consulates": cfg.consulates });
|
||||
await chrome.storage.local.set({ "ctx_consulates": ctx.consulates });
|
||||
|
||||
if (prev_cfg.activate === undefined) {
|
||||
console.log('Reading config: ' + JSON.stringify(hiddenPassword(cfg)));
|
||||
isRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
for (let key in cfg) {
|
||||
if (cfg.hasOwnProperty(key)
|
||||
&& !_.isEqual(cfg[key], prev_cfg[key])) {
|
||||
msg = `Config change: ${key}`
|
||||
if (key === 'password') {
|
||||
msg += ', ******** => ********';
|
||||
} else if (key === 'consulates') {
|
||||
msg += `, ${JSON.stringify(diffObjects(cfg[key], prev_cfg[key]))}`;
|
||||
} else {
|
||||
msg += `, ${prev_cfg[key]} => ${cfg[key]}`;
|
||||
}
|
||||
console.log(msg);
|
||||
|
||||
// reduce wait times for consulates if frequency is increased
|
||||
if (key === 'frequency') {
|
||||
let wasChanged = false;
|
||||
for (let c in ctx.consulates) {
|
||||
let newNextCheckAt = getFutureDate(cfg.frequency, cfg.frequency);
|
||||
if (ctx.consulates[c].nextCheckAt > newNextCheckAt) {
|
||||
console.log(`Reducing wait time for ${c} from ${ctx.consulates[c].nextCheckAt} to ${newNextCheckAt}`);
|
||||
ctx.consulates[c].nextCheckAt = newNextCheckAt;
|
||||
wasChanged = true;
|
||||
}
|
||||
}
|
||||
// TODO maybe causes additional requests
|
||||
if (wasChanged) {
|
||||
await chrome.storage.local.set({ "ctx_consulates": ctx.consulates });
|
||||
}
|
||||
}
|
||||
|
||||
if (key === 'activate') {
|
||||
if (cfg[key]) {
|
||||
console.log('Activating extension');
|
||||
} else {
|
||||
console.log('Deactivating extension');
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": "inactive" });
|
||||
}
|
||||
}
|
||||
|
||||
// clear signin attempts when credentials are changed
|
||||
if (key === 'username' && cfg[key]
|
||||
|| key === 'password' && cfg[key]) {
|
||||
ctx.signinAttempts = 0;
|
||||
await chrome.storage.local.set({ "ctx_signinAttempts": ctx.signinAttempts });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!cfg.activate) {
|
||||
isRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (cfg.username === "" || cfg.password === "") {
|
||||
console.log('Username or password is empty');
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": "missing credentials" });
|
||||
isRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (cfg.frequency <= 0) {
|
||||
console.log('Frequency is 0 or negative');
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": "invalid frequency" });
|
||||
isRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (CHECK_OPER_HOURS) {
|
||||
// Check if current time is between 11pm and 9am UTC (4pm - 2am PST)
|
||||
let now = new Date();
|
||||
let currentHourUTC = now.getUTCHours();
|
||||
if (currentHourUTC >= OPER_HOURS_START || currentHourUTC < OPER_HOURS_END) {
|
||||
// Continue running the code
|
||||
} else {
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": "not operational hours" });
|
||||
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({ "ctx_statusMsg": msg });
|
||||
isRunning = false;
|
||||
return;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
else if (isSignInPage()) {
|
||||
// Prevent brute forcing
|
||||
if (ctx.signinAttempts >= MAX_SIGNIN_ATTEMPTS) {
|
||||
if (prev_ctx.signinAttempts < MAX_SIGNIN_ATTEMPTS) {
|
||||
msg = 'Too many sign in attempts';
|
||||
console.log(msg);
|
||||
await sendNotification(msg);
|
||||
}
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": "too many sign in attempts" });
|
||||
isRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Sign in
|
||||
msg = 'Signing in attempt: ' + ctx.signinAttempts;
|
||||
console.log(msg)
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": msg });
|
||||
let signedIn = await enterCredentials(cfg.username, cfg.password);
|
||||
ctx.signinAttempts += 1;
|
||||
await chrome.storage.local.set({ "ctx_signinAttempts": ctx.signinAttempts });
|
||||
if (!signedIn) {
|
||||
msg = 'Failed to sign in';
|
||||
console.log(msg);
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": msg });
|
||||
isRunning = false;
|
||||
return;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
else if (isDashboardPage()) {
|
||||
// reset signin attempts when successfully logged in
|
||||
ctx.signinAttempts = 0;
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": "fetching appointment info" });
|
||||
await chrome.storage.local.set({ "ctx_signinAttempts": ctx.signinAttempts });
|
||||
|
||||
// get appointmentId
|
||||
if (!ctx.apptId) {
|
||||
ctx.apptId = await getAppointmentId();
|
||||
if (ctx.apptId) {
|
||||
console.log(`Appointment ID: ${ctx.apptId}`);
|
||||
await chrome.storage.local.set({ "ctx_apptId": ctx.apptId });
|
||||
} else {
|
||||
msg = 'No appointments found';
|
||||
console.log(msg);
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": msg });
|
||||
isRunning = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// get current appointment date
|
||||
let apptInfoCard = document.querySelector("p.consular-appt [href*='" + ctx.apptId + "']").parentNode.parentNode.parentNode
|
||||
if (!apptInfoCard.querySelector("h4").innerText.match(/Attend Appointment/)) {
|
||||
msg = 'Appointment not available';
|
||||
console.log(msg);
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": 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) {
|
||||
msg = 'Failed to fetch appointment date or consulate';
|
||||
console.log(msg);
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": msg });
|
||||
isRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (apptDate != ctx.currentAppt.date || apptConsulate != ctx.currentAppt.consulate) {
|
||||
msg = `New appointment date: ${apptDate} at ${apptConsulate}`
|
||||
if (ctx.currentAppt.date != null) {
|
||||
sendNotification(msg);
|
||||
}
|
||||
msg += `, was: ${ctx.currentAppt.consulate} at ${ctx.currentAppt.date}`
|
||||
console.log(msg);
|
||||
ctx.currentAppt = { consulate: apptConsulate, date: apptDate };
|
||||
await chrome.storage.local.set({ "ctx_currentAppt": ctx.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 (!ctx.currentAppt.date) {
|
||||
console.log('No appointment date is set, going back to dashboard');
|
||||
await goToDashboardPage();
|
||||
isRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// TODO maybe it's a rare case
|
||||
if (!document.getElementById("consulate_date_time")) {
|
||||
msg = 'No available appointments';
|
||||
console.log(msg);
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": msg });
|
||||
isRunning = false;
|
||||
return;
|
||||
};
|
||||
|
||||
if (!ctx.consulates || Object.keys(ctx.consulates).length == 0) {
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": "Fetching consulates" });
|
||||
ctx.consulates = await getConsulates();
|
||||
if (!ctx.consulates || Object.keys(ctx.consulates).length == 0) {
|
||||
msg = "No consulates found";
|
||||
console.log(msg);
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": msg });
|
||||
sendNotification(msg);
|
||||
}
|
||||
|
||||
[ctx.consulates, cfg.consulates] = ensureRequiredConsulateProperties(
|
||||
ctx.consulates, cfg.consulates, cfg.frequency
|
||||
);
|
||||
|
||||
// if no consulates are selected, select one which we have appointment with
|
||||
let selectedConsulates = Object.keys(ctx.consulates).filter(c => cfg.consulates[c].isSelected);
|
||||
if (selectedConsulates.length == 0) {
|
||||
for (let c in cfg.consulates) {
|
||||
if (c === ctx.currentAppt.consulate) {
|
||||
cfg.consulates[c].isSelected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
await chrome.storage.local.set({ "cfg_consulates": cfg.consulates });
|
||||
await chrome.storage.local.set({ "ctx_consulates": ctx.consulates });
|
||||
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(ctx.consulates).filter(c => cfg.consulates[c].isSelected);
|
||||
if (selectedConsulates.length == 0) {
|
||||
msg = 'No selected consulates found';
|
||||
console.log(msg);
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": msg });
|
||||
isRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let processedConsulates = 0;
|
||||
for (let c of selectedConsulates) {
|
||||
// only one consulate per run
|
||||
if (processedConsulates > 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset all soft bans in the first 5 minutes of the first operational hour
|
||||
let now = new Date();
|
||||
if (isNoBanTime(cfg.frequency)) {
|
||||
let nextCheckAt = new Date(ctx.consulates[c].nextCheckAt);
|
||||
let differenceInMinutes = (nextCheckAt.getTime() - now.getTime()) / (1000 * 60);
|
||||
|
||||
if (differenceInMinutes > cfg.frequency) {
|
||||
console.log(`Resetting soft ban for ${c}`);
|
||||
ctx.consulates[c].nextCheckAt = getFutureDate(0, cfg.frequency);
|
||||
}
|
||||
}
|
||||
|
||||
// skip if not time to check
|
||||
if (ctx.consulates[c].nextCheckAt > new Date().toISOString()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
processedConsulates += 1;
|
||||
|
||||
msg = `Checking dates for ${c}`;
|
||||
console.log(msg);
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": msg });
|
||||
let availDates = await getAvailableDates(ctx.consulates[c].id);
|
||||
ctx.consulates[c].nextCheckAt = getFutureDate(cfg.frequency, cfg.frequency);
|
||||
|
||||
if (!availDates) {
|
||||
msg = `Failed to fetch available dates in ${c}`;
|
||||
console.log(msg);
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": msg });
|
||||
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 ${c}`;
|
||||
console.log(msg);
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": msg });
|
||||
|
||||
if (!isNoBanTime(cfg.frequency)) {
|
||||
console.log(`Setting soft ban for ${c}`);
|
||||
ctx.consulates[c].nextCheckAt = getFutureDate(SOFT_BAN_TIMEOUT, cfg.frequency);
|
||||
}
|
||||
ctx.consulates[c].currentDate = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
msg = `Available dates for ${c}: ${availDates.slice(0, 5)}`;
|
||||
if (availDates.length > 5) {
|
||||
msg += ` and ${availDates.length - 10} more`;
|
||||
}
|
||||
console.log(msg);
|
||||
ctx.consulates[c].currentDate = availDates[0];
|
||||
if (!ctx.consulates[c].bestDate || availDates[0] < ctx.consulates[c].bestDate) {
|
||||
console.log(`New record for ${c}: ${availDates[0]}`);
|
||||
ctx.consulates[c].bestDate = availDates[0];
|
||||
}
|
||||
|
||||
// filter dates with our requests
|
||||
let filteredDates = await filterDates(availDates, ctx.currentAppt.date, cfg.deltaAppt, cfg.deltaNow);
|
||||
if (!filteredDates.length) {
|
||||
console.log(`No better dates in ${c}, currently available ${availDates[0]}`);
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": `no better dates in ${c}`});
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`Dates worth rescheduling in ${c}: ${filteredDates}`);
|
||||
let chosenDate = filteredDates[0];
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": `Found in ${c} 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(ctx.consulates[c].id, chosenDate);
|
||||
if (!availTimes) {
|
||||
msg = `Failed to fetch available timeslots in ${c} at ${chosenDate}`;
|
||||
console.log(msg);
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": msg});
|
||||
continue;
|
||||
}
|
||||
if (availTimes.length == 0) {
|
||||
msg = `No timeslots in ${c} at ${chosenDate}`;
|
||||
console.log(msg);
|
||||
await chrome.storage.local.set({ "ctx_statusMsg": msg});
|
||||
continue;
|
||||
}
|
||||
console.log(`Available timeslots in ${c} at ${chosenDate}: ${availTimes}`);
|
||||
let chosenTime = availTimes[availTimes.length - 1];
|
||||
|
||||
// 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 ${c} at ${chosenDate} ${chosenTime}`;
|
||||
console.log(msg);
|
||||
await sendNotification(msg);
|
||||
|
||||
if (!cfg.consulates[c].autoBook) {
|
||||
isFoundAppointment = true;
|
||||
} else {
|
||||
await delay(PAGE_WAIT_TIME);
|
||||
msg = `Auto booking in ${c} 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 c in ctx.consulates) {
|
||||
if (!prev_ctx?.consulates?.[c]) continue;
|
||||
if (ctx.consulates[c].nextCheckAt != prev_ctx.consulates[c].nextCheckAt) {
|
||||
console.log(`Next check for ${c} at ${ctx.consulates[c].nextCheckAt}`);
|
||||
}
|
||||
}
|
||||
await chrome.storage.local.set({ "cfg_consulates": cfg.consulates });
|
||||
await chrome.storage.local.set({ "ctx_consulates": ctx.consulates });
|
||||
isRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
else if (isConfirmationPage) {
|
||||
// go back to dashboard after successful reschedule
|
||||
await delay(PAGE_WAIT_TIME);
|
||||
ctx.currentAppt = { consulate: null, date: null};
|
||||
await chrome.storage.local.set({"ctx_currentAppt": ctx.currentAppt});
|
||||
console.log('Rescheduled successfully');
|
||||
|
||||
// switch off autoBook for all consulates after successful reschedule
|
||||
for (let c in cfg.consulates) {
|
||||
cfg.consulates[c].autoBook = false;
|
||||
}
|
||||
|
||||
await goToDashboardPage();
|
||||
}
|
||||
|
||||
// console.log('runner done');
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
setInterval(runner, 500);
|
||||
29
scripts/lodash-core.min.js
vendored
Normal file
29
scripts/lodash-core.min.js
vendored
Normal file
@ -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<u;){var o=n[e],i=t(o);if(null!=i&&(c===Z?i===i:r(i,c)))var c=i,f=o}return f}function l(n,t){var r=[];return mn(n,function(n,e,u){t(n,e,u)&&r.push(n)}),r}function p(n,r,e,u,o){var i=-1,c=n.length;for(e||(e=R),o||(o=[]);++i<c;){var f=n[i];0<r&&e(f)?1<r?p(f,r-1,e,u,o):t(o,f):u||(o[o.length]=f)}return o}function s(n,t){return n&&On(n,t,Dn);
|
||||
}function h(n,t){return l(t,function(t){return U(n[t])})}function v(n,t){return n>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 n<t}function j(n,t){var r=-1,e=M(n)?Array(n.length):[];return mn(n,function(n,u,o){e[++r]=t(n,u,o)}),e}function d(n){var t=_n(n);return function(r){var e=t.length;if(null==r)return!e;for(r=Object(r);e--;){var u=t[e];if(!(u in r&&b(n[u],r[u],3)))return false}return true}}function m(n,t){return n=Object(n),C(t,function(t,r){return r in n&&(t[r]=n[r]),t},{})}function O(n){return xn(I(n,void 0,X),n+"");
|
||||
}function x(n,t,r){var e=-1,u=n.length;for(0>t&&(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);++e<u;)r[e]=n[e+t];return r}function A(n){return x(n,0,n.length)}function E(n,t){var r;return mn(n,function(n,e,u){return r=t(n,e,u),!r}),!!r}function w(n,r){return C(r,function(n,r){return r.func.apply(r.thisArg,t([n],r.args))},n)}function k(n,t,r){var e=!r;r||(r={});for(var u=-1,o=t.length;++u<o;){var i=t[u],c=Z;if(c===Z&&(c=n[i]),e)r[i]=c;else{var f=r,a=f[i];pn.call(f,i)&&J(a,c)&&(c!==Z||i in f)||(f[i]=c);
|
||||
}}return r}function N(n){return O(function(t,r){var e=-1,u=r.length,o=1<u?r[u-1]:Z,o=3<n.length&&typeof o=="function"?(u--,o):Z;for(t=Object(t);++e<u;){var i=r[e];i&&n(t,i,e,o)}return t})}function F(n){return function(){var t=arguments,r=dn(n.prototype),t=n.apply(r,t);return V(t)?t:r}}function S(n,t,r){function e(){for(var o=-1,i=arguments.length,c=-1,f=r.length,a=Array(f+i),l=this&&this!==on&&this instanceof e?u:n;++c<f;)a[c]=r[c];for(;i--;)a[c++]=arguments[++o];return l.apply(t,a)}if(typeof n!="function")throw new TypeError("Expected a function");
|
||||
var u=F(n);return e}function T(n,t,r,e,u,o){var i=n.length,c=t.length;if(i!=c&&!(1&r&&c>i))return false;for(var c=-1,f=true,a=2&r?[]:Z;++c<i;){var l=n[c],p=t[c];if(void 0!==Z){f=false;break}if(a){if(!E(t,function(n,t){if(!P(a,t)&&(l===n||u(l,n,r,e,o)))return a.push(t)})){f=false;break}}else if(l!==p&&!u(l,p,r,e,o)){f=false;break}}return f}function B(n,t,r,e,u,o){var i=1&r,c=Dn(n),f=c.length,a=Dn(t).length;if(f!=a&&!i)return false;for(var l=f;l--;){var p=c[l];if(!(i?p in t:pn.call(t,p)))return false}for(a=true;++l<f;){var p=c[l],s=n[p],h=t[p];
|
||||
if(void 0!==Z||s!==h&&!u(s,h,r,e,o)){a=false;break}i||(i="constructor"==p)}return a&&!i&&(r=n.constructor,e=t.constructor,r!=e&&"constructor"in n&&"constructor"in t&&!(typeof r=="function"&&r instanceof r&&typeof e=="function"&&e instanceof e)&&(a=false)),a}function R(t){return Nn(t)||n(t)}function D(n){var t=[];if(null!=n)for(var r in Object(n))t.push(r);return t}function I(n,t,r){return t=jn(t===Z?n.length-1:t,0),function(){for(var e=arguments,u=-1,o=jn(e.length-t,0),i=Array(o);++u<o;)i[u]=e[t+u];for(u=-1,
|
||||
o=Array(t+1);++u<t;)o[u]=e[u];return o[t]=r(i),n.apply(this,o)}}function $(n){return(null==n?0:n.length)?p(n,1):[]}function q(n){return n&&n.length?n[0]:Z}function P(n,t,r){var e=null==n?0:n.length;r=typeof r=="number"?0>r?jn(e+r,0):r:0,r=(r||0)-1;for(var u=t===t;++r<e;){var o=n[r];if(u?o===t:o!==o)return r}return-1}function z(n,t){return mn(n,g(t))}function C(n,t,r){return e(n,g(t),r,3>arguments.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&&0==t%1&&9007199254740991>=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--:++o<u)&&false!==e(i[o],o,i););return r}}(s),On=function(n){return function(t,r,e){var u=-1,o=Object(t);e=e(t);for(var i=e.length;i--;){var c=e[n?i:++u];if(false===r(o[c],c,o))break}return t}}(),xn=X,An=function(n){return function(t,r,e){var u=Object(t);if(!M(t)){var o=g(r);t=Dn(t),r=function(n){return o(u[n],n,u)}}return r=n(t,r,e),-1<r?u[o?t[r]:r]:Z}}(function(n,t,r){var e=null==n?0:n.length;
|
||||
if(!e)return-1;r=null==r?0:Fn(r),0>r&&(r=jn(e+r,0));n:{for(t=g(t),e=n.length,r+=-1;++r<e;)if(t(n[r],r,n)){n=r;break n}n=-1}return n}),En=O(function(n,t,r){return S(n,t,r)}),wn=O(function(n,t){return c(n,1,t)}),kn=O(function(n,t,r){return c(n,Sn(t)||0,r)}),Nn=Array.isArray,Fn=Number,Sn=Number,Tn=N(function(n,t){k(t,_n(t),n)}),Bn=N(function(n,t){k(t,D(t),n)}),Rn=O(function(n,t){n=Object(n);var r,e=-1,u=t.length,o=2<u?t[2]:Z;if(r=o){r=t[0];var i=t[1];if(V(o)){var c=typeof i;if("number"==c){if(c=M(o))var c=o.length,f=typeof i,c=null==c?9007199254740991:c,c=!!c&&("number"==f||"symbol"!=f&&en.test(i))&&-1<i&&0==i%1&&i<c;
|
||||
}else c="string"==c&&i in o;r=!!c&&J(o[i],r)}else r=false}for(r&&(u=1);++e<u;)for(o=t[e],r=In(o),i=-1,c=r.length;++i<c;){var f=r[i],a=n[f];(a===Z||J(a,ln[f])&&!pn.call(n,f))&&(n[f]=o[f])}return n}),Dn=_n,In=D,$n=function(n){return xn(I(n,Z,$),n+"")}(function(n,t){return null==n?{}:m(n,t)});o.assignIn=Bn,o.before=G,o.bind=En,o.chain=function(n){return n=o(n),n.__chain__=true,n},o.compact=function(n){return l(n,Boolean)},o.concat=function(){var n=arguments.length;if(!n)return[];for(var r=Array(n-1),e=arguments[0];n--;)r[n-1]=arguments[n];
|
||||
return t(Nn(e)?A(e):[e],p(r,1))},o.create=function(n,t){var r=dn(n);return null==t?r:Tn(r,t)},o.defaults=Rn,o.defer=wn,o.delay=kn,o.filter=function(n,t){return l(n,g(t))},o.flatten=$,o.flattenDeep=function(n){return(null==n?0:n.length)?p(n,nn):[]},o.iteratee=g,o.keys=Dn,o.map=function(n,t){return j(n,g(t))},o.matches=function(n){return d(Tn({},n))},o.mixin=Y,o.negate=function(n){if(typeof n!="function")throw new TypeError("Expected a function");return function(){return!n.apply(this,arguments)}},o.once=function(n){
|
||||
return G(2,n)},o.pick=$n,o.slice=function(n,t,r){var e=null==n?0:n.length;return r=r===Z?e:+r,e?x(n,null==t?0:+t,r):[]},o.sortBy=function(n,t){var e=0;return t=g(t),j(j(n,function(n,r,u){return{value:n,index:e++,criteria:t(n,r,u)}}).sort(function(n,t){var r;n:{r=n.criteria;var e=t.criteria;if(r!==e){var u=r!==Z,o=null===r,i=r===r,c=e!==Z,f=null===e,a=e===e;if(!f&&r>e||o&&c&&a||!u&&a||!i){r=1;break n}if(!o&&r<e||f&&u&&i||!c&&i||!a){r=-1;break n}}r=0}return r||n.index-t.index}),r("value"))},o.tap=function(n,t){
|
||||
return t(n),n},o.thru=function(n,t){return t(n)},o.toArray=function(n){return M(n)?n.length?A(n):[]:W(n)},o.values=W,o.extend=Bn,Y(o,o),o.clone=function(n){return V(n)?Nn(n)?A(n):k(n,_n(n)):n},o.escape=function(n){return(n=Q(n))&&rn.test(n)?n.replace(tn,fn):n},o.every=function(n,t,r){return t=r?Z:t,f(n,g(t))},o.find=An,o.forEach=z,o.has=function(n,t){return null!=n&&pn.call(n,t)},o.head=q,o.identity=X,o.indexOf=P,o.isArguments=n,o.isArray=Nn,o.isBoolean=function(n){return true===n||false===n||H(n)&&"[object Boolean]"==hn.call(n);
|
||||
},o.isDate=function(n){return H(n)&&"[object Date]"==hn.call(n)},o.isEmpty=function(t){return M(t)&&(Nn(t)||L(t)||U(t.splice)||n(t))?!t.length:!_n(t).length},o.isEqual=function(n,t){return b(n,t)},o.isFinite=function(n){return typeof n=="number"&&gn(n)},o.isFunction=U,o.isNaN=function(n){return K(n)&&n!=+n},o.isNull=function(n){return null===n},o.isNumber=K,o.isObject=V,o.isRegExp=function(n){return H(n)&&"[object RegExp]"==hn.call(n)},o.isString=L,o.isUndefined=function(n){return n===Z},o.last=function(n){
|
||||
var t=null==n?0:n.length;return t?n[t-1]:Z},o.max=function(n){return n&&n.length?a(n,X,v):Z},o.min=function(n){return n&&n.length?a(n,X,_):Z},o.noConflict=function(){return on._===this&&(on._=vn),this},o.noop=function(){},o.reduce=C,o.result=function(n,t,r){return t=null==n?Z:n[t],t===Z&&(t=r),U(t)?t.call(n):t},o.size=function(n){return null==n?0:(n=M(n)?n:_n(n),n.length)},o.some=function(n,t,r){return t=r?Z:t,E(n,g(t))},o.uniqueId=function(n){var t=++sn;return Q(n)+t},o.each=z,o.first=q,Y(o,function(){
|
||||
var n={};return s(o,function(t,r){pn.call(o.prototype,r)||(n[r]=t)}),n}(),{chain:false}),o.VERSION="4.17.15",mn("pop join replace reverse split push shift sort splice unshift".split(" "),function(n){var t=(/^(?:replace|split)$/.test(n)?String.prototype:an)[n],r=/^(?:push|sort|unshift)$/.test(n)?"tap":"thru",e=/^(?:pop|join|replace|shift)$/.test(n);o.prototype[n]=function(){var n=arguments;if(e&&!this.__chain__){var u=this.value();return t.apply(Nn(u)?u:[],n)}return this[r](function(r){return t.apply(Nn(r)?r:[],n);
|
||||
})}}),o.prototype.toJSON=o.prototype.valueOf=o.prototype.value=function(){return w(this.__wrapped__,this.__actions__)},typeof define=="function"&&typeof define.amd=="object"&&define.amd?(on._=o, define(function(){return o})):cn?((cn.exports=o)._=o,un._=o):on._=o}).call(this);
|
||||
Loading…
Reference in New Issue
Block a user