Compare commits

...

71 Commits

Author SHA1 Message Date
3478fd5a6e Bump version 2024-08-01 15:53:22 -07:00
8697c717b8 Send notification on appointment update 2024-08-01 14:06:01 -07:00
c31ab1eb64 Use last timeslot in the selected day 2024-08-01 13:29:41 -07:00
cba2d21c7e Bump version 2024-05-02 14:27:37 -07:00
e50e3c43f6 Rework soft ban functional 2024-05-02 14:26:55 -07:00
5ddffe06e7 Change default notification channel 2024-05-02 13:16:45 -07:00
7331c266f4 Do not send notification on HTTP401 2024-05-02 13:16:12 -07:00
18c8b2112c Revert "Remove operational hours"
This reverts commit 425ff34ea0.
2024-05-02 13:15:17 -07:00
2aaaa96dcf Fix HTTP401 error 2024-04-30 16:42:52 -07:00
425ff34ea0 Remove operational hours 2024-04-30 16:41:36 -07:00
3c72a20878 Update currentDate in popup to "-" if date not found 2024-04-29 22:34:59 -07:00
4f83907506 Don't ask for address when getting dates 2024-04-29 18:43:17 -07:00
5e96252bb4 Bump version 2024-04-27 22:53:38 -07:00
17bc09d67e Probably fixed 401 error 2024-04-27 22:48:50 -07:00
6af70c07bc Reduce runner poll period to 0.5s 2024-04-27 22:48:24 -07:00
ce539b61a1 Fix entering credentials 2024-04-27 22:48:07 -07:00
ca84e18468 Send notification on MAX_LOGIN_ATTEMPTS 2024-04-27 22:47:57 -07:00
f66c6ecbf9 Close modal warning on signin page 2024-04-27 22:47:35 -07:00
f580244a5d Increase MAX_SIGNIN_ATTEMPTS to 3 2024-04-27 22:47:10 -07:00
90a58961d4 Fix live current & best dates in popup 2024-04-27 22:46:50 -07:00
3f2ba8bd2a Separate configuration and context for consulates 2024-04-27 22:10:32 -07:00
d4c8835e9b Make request identical to real requests 2024-04-27 14:05:08 -07:00
776b0afdab Bump version 2024-04-26 23:46:14 -07:00
c428e4ab11 Probably fix HTTP 401 2024-04-26 23:45:56 -07:00
d19b9ced8f Update frequency config
- Set values from 0.5 to 10 with 0.5 step
- Add random increment equal to 10% of frequency parameter
2024-04-26 23:16:59 -07:00
4a506de6fe Fix error in getPathnameParts() 2024-04-26 22:35:51 -07:00
2070cdb30a Add status for non operational hours 2024-04-26 02:53:55 -07:00
fcb9f9dbf3 Bump version 2024-04-26 02:36:22 -07:00
094e2d2b81 UI rearrangement 2024-04-26 02:36:05 -07:00
c4307472ca Smooth transitions for text on popup 2024-04-25 23:52:53 -07:00
616367a796 Improve status log 2024-04-25 23:38:03 -07:00
66e0ac47ba Live update for current/best dates 2024-04-25 23:33:16 -07:00
86c7576131 Fix missing curly brace 2024-04-25 23:18:36 -07:00
bcdcd0001a Do processing only in working hours 2024-04-25 23:14:38 -07:00
d075d8ac3e Don't move if we're found an appointment 2024-04-25 23:00:44 -07:00
15613c19fd Process only one consulate per run 2024-04-25 22:49:46 -07:00
84d608a9f4 Handle HTTP 401 2024-04-25 22:44:54 -07:00
9bbcc6e4a2 Rename lodash script filename 2024-04-24 19:48:50 -07:00
b12243afaf Improve logging 2024-04-24 19:46:05 -07:00
923dd7495b Send notification on autobook 2024-04-24 17:01:31 -07:00
20b3a15a99 Automatically update status 2024-04-24 16:59:05 -07:00
33f11a6528 Disable autobook after successful reschedule 2024-04-24 16:47:30 -07:00
32cf1a5acf Move save creds button 2024-04-24 16:33:36 -07:00
91672725fa Use table for consulates info 2024-04-24 16:33:16 -07:00
99e61bb1fc Reset consulate info on clicking reset button 2024-04-24 15:15:10 -07:00
b0b1b0f9c4 Fix soft ban timeout 2024-04-24 15:12:57 -07:00
984f3e4ceb Fix default current appointment info in popup 2024-04-24 15:12:45 -07:00
30438a68e8 Support multiple consulates 2024-04-24 15:12:19 -07:00
c5582e7662 Initial date request will depend on frequency parameter 2024-04-24 12:47:20 -07:00
ccfd6c1b9b Get rid of countdown 2024-04-24 09:52:16 -07:00
1088d46fac Limit delta parameters values 2024-04-24 09:47:20 -07:00
1bdabb88c2 Make frequency slider again 2024-04-24 09:46:56 -07:00
c86eb8470f Do not reload popup after reset 2024-04-24 08:51:43 -07:00
b693401ff4 Fix comparing nextCheckAt with previous config 2024-04-24 00:45:35 -07:00
92d9f51ab1 Use absolute times for waiting check times 2024-04-24 00:23:35 -07:00
363e842b56 Add current consulate to popup 2024-04-23 01:42:15 -07:00
9b820859c7 Add current appointment date to popup 2024-04-23 01:19:09 -07:00
968e764020 Fix updating current appointment date 2024-04-23 01:09:10 -07:00
2d3e1b01be Add best and current dates to consulates 2024-04-22 23:58:44 -07:00
c8e0028cb9 Improve status messaging 2024-04-22 23:30:55 -07:00
9f5fa888ef Add autobook feature 2024-04-22 23:20:43 -07:00
bdf4b5a7c4 Fix error with incorrect variable apptId 2024-04-22 22:04:14 -07:00
d5475e7055 Fix error with checking config changes 2024-04-22 22:03:26 -07:00
3f61d82ec5 Fix UI 2024-04-22 02:56:06 -07:00
b8ab66cd78 Reduce countdown logs 2024-04-22 00:45:51 -07:00
54ed7c302e Add notifications 2024-04-22 00:36:09 -07:00
cb9b2bf1e8 Add fetching date and time for one consulate 2024-04-22 00:19:26 -07:00
3a87f9946c Add dashboard logic 2024-04-21 17:09:33 -07:00
466c0b6b17 Fixed signing in 2024-04-20 23:50:49 -07:00
762c5fa196 First try for logging in 2024-04-20 21:03:26 -07:00
b5b6ca363d POF for retrying extension 2024-04-19 22:15:14 -07:00
14 changed files with 1145 additions and 50 deletions

View File

@ -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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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
View 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
View 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
View 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
View 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
View 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]}}({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}),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);