forked from albert-sun/bestbuy-queue-automation
-
Notifications
You must be signed in to change notification settings - Fork 0
/
script_cart.js
286 lines (245 loc) · 14.4 KB
/
script_cart.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
// ==UserScript==
// @name Best Buy Automation (Cart Saved Items)
// @namespace akito
// @version 2.1.0
// @description Auto-presses drops when button changes to Add
// @author akito#9528 / Albert Sun
// @updateURL https://raw.githubusercontent.com/albert-sun/bestbuy-queue-automation/main/script_cart.js
// @downloadURL https://raw.githubusercontent.com/albert-sun/bestbuy-queue-automation/main/script_cart.js
// @require https://raw.githubusercontent.com/albert-sun/bestbuy-queue-automation/main/utilities.js
// @require https://raw.githubusercontent.com/albert-sun/bestbuy-queue-automation/main/user_interface.js
// @require https://code.jquery.com/jquery-2.2.3.min.js
// @require https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.min.js
// @resource css https://raw.githubusercontent.com/albert-sun/bestbuy-queue-automation/main/styling.css
// @match https://www.bestbuy.com/cart
// @run-at document-start
// @grant GM_getResourceText
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant unsafeWindow
// ==/UserScript==
/* globals $, callbackArray, callbackObject, checkBlackWhitelist, edgeDetect, elementColor */
/* globals generateInterface, generateWindow, designateLogging, designateSettings */
// Version Changelog
// 1.0.2 - Added update and download URL to metadata, go to Tampermonkey settings -> enable "Check Interval" for auto-updating
// 1.1.0 - Added primite adblock detection to notify users, keyword blacklist, more config settings, and non-refresh method when adding to cart
// 2.0.0 - Complete fancy script overhaul adding primitive adblock detection and notification and removal of auto page-refreshing on actions (it was alot of work...)
// 2.1.0 - Fancy fancy new user interface (lots of hacks since Best Buy hates me), please debug and let me know how it goes!
const version = "2.1.0";
const scriptName = "bestBuy-cartSavedItems"; // For settings retrieval
const scriptText = `Best Buy (Cart Saved Items) v${version} | Albert Sun / akito#9528`;
const messageText = "Thank you and good luck! | https://github.com/albert-sun/bestbuy-queue-automation";
// Script-specific settings, please don't modify as it probably won't do anything!
// Instead, use the settings user interface implemented within the script itself.
const settings = {
checkAdblock: { description: "Primitive Adblock Check", type: "boolean", value: true },
initialClick: { description: "Auto-Add Button Clicking", type: "boolean", value: true },
colorInterval: { description: "Color Polling Interval (ms)", type: "number", value: 250 },
loadUnloadInterval: { description: "Load/Unload Polling Interval (ms)", type: "number", value: 50 },
errorResetDelay: { description: "Error Reset Delay (ms)", type: "number", value: 250 },
cartCheckDelay: { description: "Cart Checking Delay (ms)", type: "number", value: 250 },
cartSkipTimeout: { description: "Cart Skip Timeout (ms)", type: "number", value: 5000 },
};
const storage = {
buttons: {}, //_________ mapping SKU -> add button elements
colors: {}, //__________ mapping SKU -> current button color
descriptions: {}, //____ mapping SKU -> product description
intervalIDs: {}, //_____ mapping SKU -> setInterval IDs
}; // Dedicated storage variable for script usage
const adblockURLs = [
"https://pubads.g.doubleclick.net/ssai/event/",
"https://googleads.g.doubleclick.net/pagead/html/",
"https://online-metrix.net/",
]; // Test request URLs for detecting adblock status
const keyWhitelist = ["3060", "3070", "3080", "3090", "6800", "6900", "5600X", "5800X", "5900X", "5950X", "PS5"];
const keyBlacklist = []; // Temporary, blacklist > whitelist
const audio = new Audio("https://proxy.notificationsounds.com/notification-sounds/definite-555/download/file-sounds-1085-definite.mp3");
let loggingFunction = undefined; // Leave for "global" logging usage by script
// Load user interface consisting of footer and individual windows
// Return debug logging function tied to logging window for usage elsewhere
// @returns {function}
async function loadInterface() {
// Load SimpleBar and elements CSS
// Note that SimpleBar is useless for Best Buy...
const my_css = GM_getResourceText("css");
GM_addStyle(my_css);
// Load script-specific settings from Tampermonkey storage
for(const property in settings) {
const lookupKey = `${scriptName}_${property}`;
const storageValue = await GM_getValue(lookupKey, settings[property].value);
// Attach setter to settings to save any changes
// Inconvenient because it requires separate property for each child...
settings[property]._value = storageValue;
delete settings[property].value;
Object.defineProperty(settings[property], "value", {
get: function() { return settings[property]._value; },
set: function(value) {
settings[property]._value = value;
GM_setValue(lookupKey, value);
}
});
}
// Generate script footer and elements for settings and logging
generateInterface(scriptText, messageText);
const settingsDiv = generateWindow(
"https://cdn3.iconfinder.com/data/icons/google-material-design-icons/48/ic_settings_48px-512.png",
"Settings (Updates on Refresh)", // Width height ignored because compatibility mode
400, 400, true // Compatibility mode enabled
);
const loggingDiv = generateWindow(
"https://cdn2.iconfinder.com/data/icons/font-awesome/1792/code-512.png",
"Debug Logging", // Width height ignored because compatibility mode
800, 400, true // Compatibility mode enabled
);
// Designate the two windows as setting and logging windows
designateSettings(settingsDiv, settings);
const loggingFunc = designateLogging(loggingDiv);
loggingFunc("Finished initializing script user interface");
return loggingFunc;
}
// Called on cart change, clear storage and re-retrieve saved items HTMl attributes.
// Saved item elements are unloaded and reloaded with identical but different instances.
// On initial page load, skipUnload = true to skip element unloading polling.
// @param {boolean} skipUnload
// @param {boolean} fromCart
async function resetSaved(skipUnload, fromCart) {
loggingFunction(`Saved items watcher reset triggered from ${fromCart === true ? "non-" : ""}cart source`);
loggingFunction("Clearing existing watcher polling intervals and temporary storage");
// Clear existing queue polling intervals
// Then, clear storage attributes by resetting to empty object
for(const sku in storage.intervalIDs) {
clearInterval(storage.intervalIDs[sku]);
}
storage.buttons = {};
storage.colors = {};
storage.descriptions = {};
storage.intervalIDs = {};
// Periodically poll until saved items unload and reload
let savedWrappersRes;
if(fromCart) { await new Promise(r => setTimeout(r, settings.cartCheckDelay.value)); } // Extra wait, for cart or something idek anymore
if($(".removed-item-info__wrapper")[0] === undefined || skipUnload === true) { // Wait instead of polling for removed
loggingFunction("Waiting for saved item elements to unload from document");
do { // Wait for existing saved items elements to unload
savedWrappersRes = $(".saved-items__card-wrapper");
await new Promise(r => setTimeout(r, settings.loadUnloadInterval.value));
} while(savedWrappersRes.length !== 0);
loggingFunction("Finished waiting for saved item elements unloading");
} else if($(".removed-item-info__wrapper")[0] !== undefined) { // Weird edge case where elements never truly "disappear", wait instead
loggingFunction("Product removed from cart, performing alternate safety delay instead");
await new Promise(r => setTimeout(r, settings.cartSkipTimeout.value));
}
loggingFunction("Waiting for document to be populated with \"new\" saved item elements");
do { // Wait for "new" saved items elements to load
savedWrappersRes = $(".saved-items__card-wrapper");
await new Promise(r => setTimeout(r, settings.loadUnloadInterval.value));
} while(savedWrappersRes.length === 0);
loggingFunction("Finished waiting for \"new\" saved item elements to load");
// Convert selector result and parse other elements
const savedSKUs = savedWrappersRes.toArray().map(element => element.getAttribute("data-test-saved-sku"));
const descriptionElements = $(".saved-items__card-wrapper .simple-item__description").toArray();
const savedDescriptions = descriptionElements.map(element => element.innerText);
const savedButtons = $(".saved-items__card-wrapper .btn.btn-block").toArray();
loggingFunction(`${savedDescriptions.length} saved items found, filtering whitelist (currently hardcoded into script)`);
// Parse keywords of each and splice those with blacklisted or without whitelisted
let index = savedDescriptions.length;
while(index--) { // Loop in reverse to allow splicing
const description = savedDescriptions[index];
const valid = checkBlackWhitelist(description, keyBlacklist, keyWhitelist);
if(valid === false) { // Check whether the saved item is allowed
savedSKUs.splice(index, 1);
savedDescriptions.splice(index, 1);
savedButtons.splice(index, 1);
}
}
loggingFunction(`Finished, ${savedDescriptions.length} saved items remaining after filtering`);
loggingFunction(`Iterating through remaining elements for button clicking and interval setup`);
// Iterate through buttons and deduce whether addable, queued, or unavailable
// If addable, click the button (hopefully error element detector callback triggers)
// If queued, store relevant info, initiate setInterval, and initiate edge detection
let anyClicked = false;
for(const index in savedButtons) {
const button = savedButtons[index];
const buttonColor = elementColor(button);
const description = savedDescriptions[index];
const available = !button.classList.contains("disabled");
if((buttonColor === "white" || buttonColor === "blue") && settings.initialClick.value) { // Addable, should be available?
loggingFunction(`[${description}] currently addable, clicking and refreshing after traversal`);
button.click();
anyClicked = true;
} else if(buttonColor === "grey" && available === true) { // Currently queued
if(anyClicked === true) {
loggingFunction(`[${description}] currently in queue but not adding pending refresh`);
continue;
}
loggingFunction(`[${description}] currently in queue, attaching check interval and callback`);
// Store relevant info into storage
const sku = savedSKUs[index];
storage.buttons[sku] = button;
storage.colors[sku] = "grey";
storage.descriptions[sku] = savedDescriptions[index];
// Initiate periodic interval for updating element color
// Remember that intervals are cleared on each resetSaved call
storage.intervalIDs[sku] = setInterval(function() {
storage.colors[sku] = elementColor(storage.buttons[sku]);
}, settings.colorInterval.value);
// Initiate edge detection for callback color changes
// I think it turns to white? Does it sometimes turn yellow as well?
edgeDetect(storage.colors, sku, "grey", ["white", "yellow"], function() {
loggingFunction(`Queue for [${description}] popped detecting grey->white/yellow transition, playing audio and clicking button`);
audio.play();
button.click();
});
}
}
// Force reload if any buttons are clicked to ensure status update
if(anyClicked === true) {
location.reload();
}
}
(async function() {
'use strict';
// Load user interface including footer and windows
loggingFunction = await loadInterface();
// Primitive adblock check: check for failure when fetching commonly blocked domains
let adblockDetected = undefined;
if(settings.checkAdblock.value === true) {
const promises = adblockURLs.map(url => fetch(url));
const results = await Promise.allSettled(promises);
const failed = results.filter(x => x.status !== "fulfilled");
adblockDetected = failed.length > 0;
loggingFunction("Finished primitive adblock check");
if(adblockDetected === true) {
loggingFunction("/!\\ Adblock detected, please disable for maximum website compatibility");
}
}
addEventListener('DOMContentLoaded', async function() {
loggingFunction("Setting-up reset callback on error message detection");
// Force refresh of saved item elements whenever error detected using jQuery
// Wait some time interval before refreshing to let auto-clicker finish clicking in runtime
$(".alerts__order").bind('DOMNodeInserted', async function(e) {
await new Promise(r => setTimeout(r, settings.errorResetDelay.value));
await resetSaved(false, false);
});
loggingFunction("Performing initial setup of saved items watcher (no bundles currently, sorry!)");
await resetSaved(true, false); // Initial call on page load
loggingFunction("Setting-up reset callback on cart status change (including pickup/shipping modification)");
// Force refresh of saved item elements whenever order summary changes (cart addition / removal?)
// window.cart extraordinarily slippery, unable to hook getters/setters or anything
// Currently triggers on picking/shipping swaps but don't want custom callback function...
callbackObject(__META_LAYER_META_DATA, "order", function() { resetSaved(false, true) }, "set");
});
}());
/*
== Current Bugs ==
- Clicking on "Please Wait" breaks periodic interval check
- Must reload page sometimes after clicking initial add because no "Please Wait" transition
== Current Testing Checklist ==
[X] Adblock detection user notification
[ ] Error message element DOM insert callback
[X] Cart addition / removal (/ fulfillment swap) callback
[X] Correct whitelist and blacklist keyword product filtering and splicing
[X] Correct added / queued / unavailable element detection
[X] Dummy queued availability callback (audio + click)
[X] Reset routine on cart addition / removal (/ fulfillment swap)
*/