Tesco Delivery Slots

Makes it easier to shop for groceries on tesco.com

  1. // ==UserScript==
  2. // @name Tesco Delivery Slots
  3. // @author Than
  4. // @version 0.02
  5. // @description Makes it easier to shop for groceries on tesco.com
  6. // @match https://*.tesco.com/groceries/*
  7. // @include https://*.tesco.com/groceries/*
  8. // @include https://secure.tesco.com/account/en-GB/login*
  9. // @connect tesco.com
  10. // @grant GM.xmlHttpRequest
  11. // @grant unsafeWindow
  12. // @grant GM_addStyle
  13. // @grant GM_notification
  14. // @grant GM_setClipboard
  15. // @run-at document-end
  16. // @namespace https://gf.qytechs.cn/users/288098
  17. // ==/UserScript==
  18.  
  19.  
  20. (function() {
  21. // 'use strict';
  22. try {
  23. /*--------------------------------------------------------------------------------------------------------------------
  24. ------------------------------------------- General functions --------------------------------------------------
  25. --------------------------------------------------------------------------------------------------------------------*/
  26. // 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 } };
  27. //Check the DOM for changes and run a callback function on each mutation
  28. function observeDOM(callback){
  29. var mutationObserver = new MutationObserver(function(mutations) { //https://davidwalsh.name/mutationobserver-api
  30. mutations.forEach(function(mutation) {
  31. callback(mutation) // run the user-supplied callback function,
  32. });
  33. });
  34. // Keep an eye on the DOM for changes
  35. mutationObserver.observe(document.body, { //https://blog.sessionstack.com/how-javascript-works-tracking-changes-in-the-dom-using-mutationobserver-86adc7446401
  36. attributes: true,
  37. // characterData: true,
  38. childList: true,
  39. subtree: true,
  40. // attributeOldValue: true,
  41. // characterDataOldValue: true,
  42. attributeFilter: ["class"] // We're really only interested in stuff that has a className
  43. });}
  44.  
  45. function convertKtoM(price){ // converts kg to g, for example
  46. return (price / 10).toFixed(2);
  47. }
  48. function percentColour(percent){ // 100% = red, 0% = green
  49. var color = 'rgb(' + (percent *2.56) +',' + ((100 - percent) *2.96) +',0)'
  50. return color;
  51. }
  52. function getRndInteger(min, max) {
  53. return Math.floor(Math.random() * (max - min + 1) ) + min;
  54. }
  55.  
  56. /*--------------------------------------------------------------------------------------------------------------------
  57. ------------------------------------------- Init functions --------------------------------------------------
  58. --------------------------------------------------------------------------------------------------------------------*/
  59. if (!localStorage.openSlotsTimes){localStorage.openSlotsTimes = JSON.stringify([])}
  60. if (!localStorage.openSlots){localStorage.openSlots = JSON.stringify([])}
  61. if (!localStorage.errors){localStorage.errors = JSON.stringify([])}
  62. function sleep(ms) { // usage: await sleep(4000)
  63. return new Promise(resolve => setTimeout(resolve, ms));
  64. }
  65. var gmFetch = {} // https://www.vojtechruzicka.com/javascript-async-await/ AND https://gomakethings.com/promise-based-xhr/
  66. gmFetch.get = function (address,headers,anonymous) {
  67. return new Promise((resolve, reject) => {
  68. if (!headers){headers = ""}
  69. anonymous = anonymous ? anonymous : false;
  70. GM.xmlHttpRequest({
  71. method: "GET",
  72. url: address,
  73. headers: headers,
  74. anonymous: anonymous,
  75. onload: e => resolve(e.response),
  76. onerror: reject,
  77. ontimeout: reject,
  78. });
  79. });
  80. }
  81. gmFetch.post = function (address,postData,headers,anonymous,simple=true) {
  82. return new Promise((resolve, reject) => {
  83. if (!headers){headers = {"Content-Type": "application/x-www-form-urlencoded"}}
  84. anonymous = anonymous ? anonymous : false;
  85. GM.xmlHttpRequest({
  86. method: "POST",
  87. url: address,
  88. headers: headers,
  89. data: postData,
  90. anonymous: anonymous,
  91. onload: (simple ? e => resolve(e.response) : e => resolve(e)),
  92. onerror: reject,
  93. ontimeout: reject,
  94. });
  95. });
  96. }
  97. async function checkSlotsXhr(){
  98. if (!document.querySelector("a[class*=slot-selector]")){return} // this querySelector signifies that we are on the slot page
  99. document.querySelector("a[class*=slot-selector]").scrollIntoView(); // scroll the slots into view just so we can identify whether there are slots
  100. var headers = { // headers will be the same for everey request
  101. "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:75.0) Gecko/20100101 Firefox/75.0",
  102. "Accept": "application/json",
  103. "Accept-Language": "en-US,en;q=0.5",
  104. "X-Requested-With": "XMLHttpRequest",
  105. "Content-Type": "application/json",
  106. "ADRUM": "isAjax:true",
  107. }
  108. var slotTabs = document.querySelectorAll("a[class*=slot-selector]"); // get all the week selection tabs
  109. var slotUrls = getSlotUrls(slotTabs); // and the urls from each
  110. var slotDates = getSlotDates(slotUrls);
  111. while (true){
  112. await visitEachSlotUrl(slotDates)
  113. }
  114. // var avaiableSlots = await visitEachSlotUrl(slotUrls); // runs the function which checks the responses for slots
  115. async function visitEachSlotUrl(slotDates){
  116. for (var i=0,j = slotDates.length;i<j;i++){ // for each url
  117. var randomNumber = getRndInteger(22000, 45000) // generates a random millisecond around 6000;
  118. // randomNumber =3;
  119. console.log(`waiting for ${randomNumber} ms`);
  120. await sleep(randomNumber); // first do nothing for a little while
  121. var address = "https://www.tesco.com/groceries/en-GB/resources";
  122. var weekBeginning = slotDates[i]; // the current address of this loop
  123. var postData = getPostData(weekBeginning);
  124. var headers = getHeaders();
  125. console.log(address);
  126. console.log(postData);
  127. console.log(headers);
  128. var response = await gmFetch.post(address,postData,headers); // get the response
  129. console.log(response);
  130. //redirect-to
  131. if (!response.includes("slots")){
  132. console.log(response);
  133. GM_notification("Error? What's going on");
  134. }
  135. if (response.includes("redirect-to")){
  136. console.log("Error - we got redirected. Slowing things down a little.");
  137. console.log(response);
  138. await sleep(60000); // give it a minute
  139. location.reload(); // reload the whole page
  140. return;
  141. }
  142. if (response.includes("503 Service Temporarily Unavailable")){location.reload();} // reload, to trigger more frequent reloads
  143. console.log(response);
  144. var json = JSON.parse(response); // parse it
  145. console.log(json);
  146. var slots = json.slot.data.slots; // get the slot array
  147. var slotAvailable = searchForAvailable(slots); // returns true or false depending on whether this group has slots
  148. console.log(slotAvailable);
  149. if (slotAvailable){ // if it returns true
  150. var existingSlotInfo = JSON.parse(localStorage.openSlots);
  151. existingSlotInfo.push(json);
  152. localStorage.openSlots = JSON.stringify(existingSlotInfo);
  153. var existingTimeInfo = JSON.parse(localStorage.openSlotsTimes);
  154. existingTimeInfo.push(Date());
  155. localStorage.openSlotsTimes = JSON.stringify(existingTimeInfo);
  156. var selectorHref = json.selectedDate + "?slotGroup=1"; // get the css selector we're gonna click on
  157. GM_notification("Slots available!"); // alert them 3 times in case they miss it
  158. document.querySelector(`[href*='${weekBeginning}']`).click(); // click on the corrosponding week for the user's convenience
  159. await sleep(8000);
  160. GM_notification("Slots available!");
  161. await sleep(8000);
  162. GM_notification("Slots available!");
  163. await sleep(8000);
  164. return; // quit this function so we're just sitting on the week with the first available slot
  165. }
  166. }
  167. function searchForAvailable(slots){
  168.  
  169. for (var i=0,j = slots.length;i<j;i++){ // for each slot in this array
  170. if (slots[i].status != "UnAvailable" && slots[i].status != "Unavailable" && slots[i].status != "Booked"){ // if the slot isn't marked "unavailable" or already "Booked"
  171. checkIfSpecificDateWanted(slots[i].start);
  172. if (checkIfSpecificDateWanted(slots[i].start)){ // but now we want to search a specific date.
  173. console.log(slots[i]) // log it
  174. return true; // quit this whole function cos we got one
  175. }
  176. }
  177. }
  178. return false; // otherwise, if we get through all the loops without an open slot, return false
  179. function checkIfSpecificDateWanted(date){
  180. var wantedSlotsArray = [
  181. "2020",
  182. "2021",
  183. "2022",
  184. "2023",
  185. "2024",
  186. ]
  187. for (var i=0,j = wantedSlotsArray.length;i<j;i++){
  188. if (date.includes(wantedSlotsArray[i])){
  189. return true
  190. }
  191. }
  192. }
  193. }
  194. }
  195. function getHeaders(){
  196. var csrfToken = document.querySelector("input[name=_csrf]").value;
  197. var headers = {
  198. "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:81.0) Gecko/20100101 Firefox/81.0",
  199. "Accept": "application/json",
  200. "Accept-Language": "en-US,en;q=0.5",
  201. "X-Requested-With": "XMLHttpRequest",
  202. "Content-Type": "application/json",
  203. "x-csrf-token": csrfToken,
  204. }
  205. return headers;
  206. }
  207. function getPostData(date){
  208. var finalPostData = {
  209. "acceptWaitingRoom": false,
  210. "resources": [
  211. {
  212. "type": "slot",
  213. "params": {
  214. "query": {
  215. "locationId": null,
  216. "postcode": "",
  217. "slotGroup": 1
  218. },
  219. "shoppingMethod": "delivery",
  220. "date": date
  221. },
  222. "hash": "8864683814091659"
  223. },
  224. {
  225. "type": "fulfilmentMetadata",
  226. "params": {
  227. "shoppingMethod": "delivery"
  228. },
  229. "hash": "478896531623469"
  230. },
  231. {
  232. "type": "trolleyContents",
  233. "params": {},
  234. "hash": "8812624788225991"
  235. }
  236. ],
  237. "sharedParams": {
  238. "date": date,
  239. "shoppingMethod": "delivery",
  240. "query": {
  241. "locationId": null,
  242. "postcode": "",
  243. "slotGroup": 1
  244. }
  245. }
  246. }
  247. return JSON.stringify(finalPostData);
  248. }
  249. function getSlotUrls(slots){
  250. var urlArray = []
  251. for (var i=0,j = slots.length;i<j;i++){
  252. urlArray.push(slots[i].href);
  253. }
  254. return urlArray;
  255. }
  256. }
  257. checkSlotsXhr();
  258. // checkForSlots();
  259. checkForQueue();
  260. function getSlotDates(slotUrls){
  261. var finalArray = []
  262. for (var i=0,j = slotUrls.length;i<j;i++){
  263. finalArray.push(slotUrls[i].match(/\d\d\d\d-\d\d-\d\d/)[0])
  264. }
  265. return finalArray;
  266. }
  267. function reloadPageAfter(seconds){
  268. setTimeout(function() {
  269. location.reload();
  270. }, (seconds * 1000));
  271. }
  272.  
  273. async function checkForQueue(){
  274. var currentHour = Date().split("2020 ")[1].split(":")[0];
  275. var randomNumber = getRndInteger(600, 900) // generates a number around 160 or so by default
  276. var pageText = document.body.textContent;
  277. if (currentHour == 23){ // it's 11pm
  278. randomNumber = getRndInteger(230, 270); // reload more frequently
  279. }
  280. if (pageText.includes("now in a queue")){
  281. GM_notification("In a queue!");
  282. randomNumber = getRndInteger(65, 79); // reload about once a minute, in case it doesn't happen automatically
  283. }
  284. if (pageText.includes("Sign in to your account")){ // let's autologin
  285. await sleep(16000);
  286. randomNumber = 5;
  287. document.querySelector(".ui-component__button").click(); // click the login button
  288. }
  289. if (pageText.includes("503 Service Temporarily Unavailable")){
  290. randomNumber = getRndInteger(20, 31); // reload more frequently to make sure we get back in.
  291. }
  292. if (pageText.includes("Oops, something went wrong! (504)")){
  293. randomNumber = getRndInteger(20, 31); // reload more frequently to make sure we get back in.
  294. }
  295. console.log(`Reloading after ${randomNumber} seconds`)
  296. reloadPageAfter(randomNumber);
  297. }
  298. function checkForSlots(){
  299. return; // don't need this right now
  300. if (!document.querySelector("a[class*=slot-selector]")){return} // this signifies that we are on the slot page
  301. var firstSlot = document.querySelector("a[class*=slot-selector]").textContent
  302. var lastSlot = document.querySelectorAll("a[class*=slot-selector]")[2].textContent
  303. console.log(firstSlot);
  304. if (!firstSlot.includes("Apr 06 - 12")){
  305. GM_notification("New Slots! " + lastSlot);
  306. beep();
  307. }
  308. reloadPageAfter(30);
  309. }
  310. function beep() {
  311. 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=");
  312. snd.play();
  313. }
  314. }
  315. catch(err){console.log(err);
  316. var existingErrors = JSON.parse(localStorage.errors);
  317. existingErrors.push(err);
  318. localStorage.errors = JSON.stringify(existingErrors);
  319. }
  320. // Your code here...
  321. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址