- // ==UserScript==
- // @name Tesco Delivery Slots
- // @author Than
- // @version 0.02
- // @description Makes it easier to shop for groceries on tesco.com
- // @match https://*.tesco.com/groceries/*
- // @include https://*.tesco.com/groceries/*
- // @include https://secure.tesco.com/account/en-GB/login*
- // @connect tesco.com
- // @grant GM.xmlHttpRequest
- // @grant unsafeWindow
- // @grant GM_addStyle
- // @grant GM_notification
- // @grant GM_setClipboard
- // @run-at document-end
- // @namespace https://gf.qytechs.cn/users/288098
- // ==/UserScript==
-
-
- (function() {
- // 'use strict';
- try {
- /*--------------------------------------------------------------------------------------------------------------------
- ------------------------------------------- General functions --------------------------------------------------
- --------------------------------------------------------------------------------------------------------------------*/
- // var Email = { send: function (e, o, t, n, a, s, r, c) { var d = Math.floor(1e6 * Math.random() + 1), i = "From=" + e; i += "&to=" + o, i += "&Subject=" + encodeURIComponent(t), i += "&Body=" + encodeURIComponent(n), void 0 == a.token ? (i += "&Host=" + a, i += "&Username=" + s, i += "&Password=" + r, i += "&Action=Send") : (i += "&SecureToken=" + a.token, i += "&Action=SendFromStored", c = a.callback), i += "&cachebuster=" + d, Email.ajaxPost("https://smtpjs.com/v2/smtp.aspx?", i, c) }, sendWithAttachment: function (e, o, t, n, a, s, r, c, d) { var i = Math.floor(1e6 * Math.random() + 1), m = "From=" + e; m += "&to=" + o, m += "&Subject=" + encodeURIComponent(t), m += "&Body=" + encodeURIComponent(n), m += "&Attachment=" + encodeURIComponent(c), void 0 == a.token ? (m += "&Host=" + a, m += "&Username=" + s, m += "&Password=" + r, m += "&Action=Send") : (m += "&SecureToken=" + a.token, m += "&Action=SendFromStored"), m += "&cachebuster=" + i, Email.ajaxPost("https://smtpjs.com/v2/smtp.aspx?", m, d) }, ajaxPost: function (e, o, t) { var n = Email.createCORSRequest("POST", e); n.setRequestHeader("Content-type", "application/x-www-form-urlencoded"), n.onload = function () { var e = n.responseText; void 0 != t && t(e) }, n.send(o) }, ajax: function (e, o) { var t = Email.createCORSRequest("GET", e); t.onload = function () { var e = t.responseText; void 0 != o && o(e) }, t.send() }, createCORSRequest: function (e, o) { var t = new GM.xmlHttpRequest; return "withCredentials" in t ? t.open(e, o, !0) : "undefined" != typeof XDomainRequest ? (t = new XDomainRequest).open(e, o) : t = null, t } };
- //Check the DOM for changes and run a callback function on each mutation
- function observeDOM(callback){
- var mutationObserver = new MutationObserver(function(mutations) { //https://davidwalsh.name/mutationobserver-api
- mutations.forEach(function(mutation) {
- callback(mutation) // run the user-supplied callback function,
- });
- });
- // Keep an eye on the DOM for changes
- mutationObserver.observe(document.body, { //https://blog.sessionstack.com/how-javascript-works-tracking-changes-in-the-dom-using-mutationobserver-86adc7446401
- attributes: true,
- // characterData: true,
- childList: true,
- subtree: true,
- // attributeOldValue: true,
- // characterDataOldValue: true,
- attributeFilter: ["class"] // We're really only interested in stuff that has a className
- });}
-
- function convertKtoM(price){ // converts kg to g, for example
- return (price / 10).toFixed(2);
- }
- function percentColour(percent){ // 100% = red, 0% = green
- var color = 'rgb(' + (percent *2.56) +',' + ((100 - percent) *2.96) +',0)'
- return color;
- }
- function getRndInteger(min, max) {
- return Math.floor(Math.random() * (max - min + 1) ) + min;
- }
-
- /*--------------------------------------------------------------------------------------------------------------------
- ------------------------------------------- Init functions --------------------------------------------------
- --------------------------------------------------------------------------------------------------------------------*/
- if (!localStorage.openSlotsTimes){localStorage.openSlotsTimes = JSON.stringify([])}
- if (!localStorage.openSlots){localStorage.openSlots = JSON.stringify([])}
- if (!localStorage.errors){localStorage.errors = JSON.stringify([])}
- function sleep(ms) { // usage: await sleep(4000)
- return new Promise(resolve => setTimeout(resolve, ms));
- }
- var gmFetch = {} // https://www.vojtechruzicka.com/javascript-async-await/ AND https://gomakethings.com/promise-based-xhr/
- gmFetch.get = function (address,headers,anonymous) {
- return new Promise((resolve, reject) => {
- if (!headers){headers = ""}
- anonymous = anonymous ? anonymous : false;
- GM.xmlHttpRequest({
- method: "GET",
- url: address,
- headers: headers,
- anonymous: anonymous,
- onload: e => resolve(e.response),
- onerror: reject,
- ontimeout: reject,
- });
- });
- }
- gmFetch.post = function (address,postData,headers,anonymous,simple=true) {
- return new Promise((resolve, reject) => {
- if (!headers){headers = {"Content-Type": "application/x-www-form-urlencoded"}}
- anonymous = anonymous ? anonymous : false;
- GM.xmlHttpRequest({
- method: "POST",
- url: address,
- headers: headers,
- data: postData,
- anonymous: anonymous,
- onload: (simple ? e => resolve(e.response) : e => resolve(e)),
- onerror: reject,
- ontimeout: reject,
- });
- });
- }
- async function checkSlotsXhr(){
- if (!document.querySelector("a[class*=slot-selector]")){return} // this querySelector signifies that we are on the slot page
- document.querySelector("a[class*=slot-selector]").scrollIntoView(); // scroll the slots into view just so we can identify whether there are slots
- var headers = { // headers will be the same for everey request
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:75.0) Gecko/20100101 Firefox/75.0",
- "Accept": "application/json",
- "Accept-Language": "en-US,en;q=0.5",
- "X-Requested-With": "XMLHttpRequest",
- "Content-Type": "application/json",
- "ADRUM": "isAjax:true",
- }
- var slotTabs = document.querySelectorAll("a[class*=slot-selector]"); // get all the week selection tabs
- var slotUrls = getSlotUrls(slotTabs); // and the urls from each
- var slotDates = getSlotDates(slotUrls);
- while (true){
- await visitEachSlotUrl(slotDates)
- }
- // var avaiableSlots = await visitEachSlotUrl(slotUrls); // runs the function which checks the responses for slots
- async function visitEachSlotUrl(slotDates){
- for (var i=0,j = slotDates.length;i<j;i++){ // for each url
- var randomNumber = getRndInteger(22000, 45000) // generates a random millisecond around 6000;
- // randomNumber =3;
- console.log(`waiting for ${randomNumber} ms`);
- await sleep(randomNumber); // first do nothing for a little while
- var address = "https://www.tesco.com/groceries/en-GB/resources";
- var weekBeginning = slotDates[i]; // the current address of this loop
- var postData = getPostData(weekBeginning);
- var headers = getHeaders();
- console.log(address);
- console.log(postData);
- console.log(headers);
- var response = await gmFetch.post(address,postData,headers); // get the response
- console.log(response);
- //redirect-to
- if (!response.includes("slots")){
- console.log(response);
- GM_notification("Error? What's going on");
- }
- if (response.includes("redirect-to")){
- console.log("Error - we got redirected. Slowing things down a little.");
- console.log(response);
- await sleep(60000); // give it a minute
- location.reload(); // reload the whole page
- return;
- }
- if (response.includes("503 Service Temporarily Unavailable")){location.reload();} // reload, to trigger more frequent reloads
- console.log(response);
- var json = JSON.parse(response); // parse it
- console.log(json);
- var slots = json.slot.data.slots; // get the slot array
- var slotAvailable = searchForAvailable(slots); // returns true or false depending on whether this group has slots
- console.log(slotAvailable);
- if (slotAvailable){ // if it returns true
- var existingSlotInfo = JSON.parse(localStorage.openSlots);
- existingSlotInfo.push(json);
- localStorage.openSlots = JSON.stringify(existingSlotInfo);
- var existingTimeInfo = JSON.parse(localStorage.openSlotsTimes);
- existingTimeInfo.push(Date());
- localStorage.openSlotsTimes = JSON.stringify(existingTimeInfo);
- var selectorHref = json.selectedDate + "?slotGroup=1"; // get the css selector we're gonna click on
- GM_notification("Slots available!"); // alert them 3 times in case they miss it
- document.querySelector(`[href*='${weekBeginning}']`).click(); // click on the corrosponding week for the user's convenience
- await sleep(8000);
- GM_notification("Slots available!");
- await sleep(8000);
- GM_notification("Slots available!");
- await sleep(8000);
- return; // quit this function so we're just sitting on the week with the first available slot
- }
- }
- function searchForAvailable(slots){
-
- for (var i=0,j = slots.length;i<j;i++){ // for each slot in this array
- if (slots[i].status != "UnAvailable" && slots[i].status != "Unavailable" && slots[i].status != "Booked"){ // if the slot isn't marked "unavailable" or already "Booked"
- checkIfSpecificDateWanted(slots[i].start);
- if (checkIfSpecificDateWanted(slots[i].start)){ // but now we want to search a specific date.
- console.log(slots[i]) // log it
- return true; // quit this whole function cos we got one
- }
- }
- }
- return false; // otherwise, if we get through all the loops without an open slot, return false
- function checkIfSpecificDateWanted(date){
- var wantedSlotsArray = [
- "2020",
- "2021",
- "2022",
- "2023",
- "2024",
- ]
- for (var i=0,j = wantedSlotsArray.length;i<j;i++){
- if (date.includes(wantedSlotsArray[i])){
- return true
- }
- }
- }
- }
- }
- function getHeaders(){
- var csrfToken = document.querySelector("input[name=_csrf]").value;
- var headers = {
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:81.0) Gecko/20100101 Firefox/81.0",
- "Accept": "application/json",
- "Accept-Language": "en-US,en;q=0.5",
- "X-Requested-With": "XMLHttpRequest",
- "Content-Type": "application/json",
- "x-csrf-token": csrfToken,
- }
- return headers;
- }
- function getPostData(date){
- var finalPostData = {
- "acceptWaitingRoom": false,
- "resources": [
- {
- "type": "slot",
- "params": {
- "query": {
- "locationId": null,
- "postcode": "",
- "slotGroup": 1
- },
- "shoppingMethod": "delivery",
- "date": date
- },
- "hash": "8864683814091659"
- },
- {
- "type": "fulfilmentMetadata",
- "params": {
- "shoppingMethod": "delivery"
- },
- "hash": "478896531623469"
- },
- {
- "type": "trolleyContents",
- "params": {},
- "hash": "8812624788225991"
- }
- ],
- "sharedParams": {
- "date": date,
- "shoppingMethod": "delivery",
- "query": {
- "locationId": null,
- "postcode": "",
- "slotGroup": 1
- }
- }
- }
- return JSON.stringify(finalPostData);
- }
- function getSlotUrls(slots){
- var urlArray = []
- for (var i=0,j = slots.length;i<j;i++){
- urlArray.push(slots[i].href);
- }
- return urlArray;
- }
- }
- checkSlotsXhr();
- // checkForSlots();
- checkForQueue();
- function getSlotDates(slotUrls){
- var finalArray = []
- for (var i=0,j = slotUrls.length;i<j;i++){
- finalArray.push(slotUrls[i].match(/\d\d\d\d-\d\d-\d\d/)[0])
- }
- return finalArray;
- }
- function reloadPageAfter(seconds){
- setTimeout(function() {
- location.reload();
- }, (seconds * 1000));
- }
-
- async function checkForQueue(){
- var currentHour = Date().split("2020 ")[1].split(":")[0];
- var randomNumber = getRndInteger(600, 900) // generates a number around 160 or so by default
- var pageText = document.body.textContent;
- if (currentHour == 23){ // it's 11pm
- randomNumber = getRndInteger(230, 270); // reload more frequently
- }
- if (pageText.includes("now in a queue")){
- GM_notification("In a queue!");
- randomNumber = getRndInteger(65, 79); // reload about once a minute, in case it doesn't happen automatically
- }
- if (pageText.includes("Sign in to your account")){ // let's autologin
- await sleep(16000);
- randomNumber = 5;
- document.querySelector(".ui-component__button").click(); // click the login button
- }
- if (pageText.includes("503 Service Temporarily Unavailable")){
- randomNumber = getRndInteger(20, 31); // reload more frequently to make sure we get back in.
- }
- if (pageText.includes("Oops, something went wrong! (504)")){
- randomNumber = getRndInteger(20, 31); // reload more frequently to make sure we get back in.
- }
- console.log(`Reloading after ${randomNumber} seconds`)
- reloadPageAfter(randomNumber);
- }
- function checkForSlots(){
- return; // don't need this right now
- if (!document.querySelector("a[class*=slot-selector]")){return} // this signifies that we are on the slot page
- var firstSlot = document.querySelector("a[class*=slot-selector]").textContent
- var lastSlot = document.querySelectorAll("a[class*=slot-selector]")[2].textContent
- console.log(firstSlot);
- if (!firstSlot.includes("Apr 06 - 12")){
- GM_notification("New Slots! " + lastSlot);
- beep();
- }
- reloadPageAfter(30);
- }
- function beep() {
- var snd = new Audio("data:audio/wav;base64,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qJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=");
- snd.play();
- }
- }
- catch(err){console.log(err);
- var existingErrors = JSON.parse(localStorage.errors);
- existingErrors.push(err);
- localStorage.errors = JSON.stringify(existingErrors);
- }
- // Your code here...
- })();