// ==UserScript== // @name zevent-place-overlay // @namespace http://tampermonkey.net/ // @license MIT // @version 1.8 // @description Please organize with other participants on Discord: https://discord.gg/sXe5aVW2jV ; Press H to hide/show again the overlay. // @author ludolpif, ventston // @match https://place.zevent.fr/ // @icon https://www.google.com/s2/favicons?sz=64&domain=zevent.fr // @grant none // @downloadURL https://raw.githubusercontent.com/ludolpif/overlay-zevent-place/main/browser-script/zevent-place-overlay.user.js // @updateURL https://raw.githubusercontent.com/ludolpif/overlay-zevent-place/main/browser-script/zevent-place-overlay.user.js // ==/UserScript== /* * Script used as base, form MinusKube: https://greasyfork.org/fr/scripts/444833-z-place-overlay/code * Original and this code licence: MIT * Copyright 2021-2022 ludolpif, ventston * Thanks to : grewa, BunlanG|Baron for help on CSS */ (function() { 'use strict'; const version = "1.8"; const scriptUpdateURL = "https://raw.githubusercontent.com/ludolpif/overlay-zevent-place/main/browser-script/zevent-place-overlay.user.js" // Global constants and variables for our script const overlayJSON1 = "https://timeforzevent.fr/overlay.json"; // Need CORS header (Access-Control-Allow-Origin: https://place.zevent.fr) const overlayJSON2 = "https://backup.place.timeforzevent.fr/overlay.json"; const inviteDiscordURL = "https://discord.gg/sXe5aVW2jV"; let refreshOverlaysState = true; // state false: idle, true: asked, (no cooldown: throttled by keepOurselfInDOM()) let safeModeDisableUI = false; let safeModeDisableGetJSON = false; let wantedOverlays = {}; // Same format as knownOverlays : the format of overlay.json let refreshKnownOverlaysState = 0; // state 0: idle, 1: asked, 2: in progress (main url), 3: in progress (bkp url), 4: cooldown (rate limiting) let lastCustomId = 0; function zpoLog(msg) { const ts = new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit', second: '2-digit'}); console.log(ts + " zevent-place-overlay: " + msg); } zpoLog("version " + version); /* * FR: Utilisateurs du script: vous pouvez éditer les lignes loadOverlay() ci-après pour mémoriser dans votre navigateur * vos choix d'overlay sans utiliser le menu "Overlays" proposé par ce script sur https://place.zevent.fr/ * Pour ce faire : * 0) S'assurer que vous lisez ça depuis un onglet de l'extension TamperMonkey dans votre navigateur * (sinon vous avez manqué des étapes de la documentation sous README.md: https://github.com/ludolpif/overlay-zevent-place ) * 1) Utilisez une ligne //loadOveray(...); laissée en exemple * 2) SÉCURITÉ: ne tentez pas de charger autre chose qu'une image .png * 3) Remplacez l'URL d'exemple par l'URL de l'overlay de voter choix * 4) Enlevez le double-slash // avant loadOverlay(...); pour activer cette ligne de code * 5) Sauvez le script (Ctrl+S) * 6) Fermez cet onglet (editeur Tampermonkey) * 7) Allez sur l'onglet de https://place.zevent.fr et rafraichissez avec Ctrl+R * Remarques : * - Les calques (overlays) ne s'affichent qu'après l'authentification sur le site https://place.zevent.fr * - Ne touchez pas / préservez les point-virgules en fin de ligne de code, le script tombe en panne sinon. * Mode sans échec : * - Si vous avez un bug avec l'UI, mettez vos URL dans des lignes loadOverlay(...); * et enlevez le // devant la ligne //safeModeDisableUI... */ //safeModeDisableGetJSON = true; //safeModeDisableUI = true; //loadOverlay("https://raw.githubusercontent.com/ludolpif/overlay-zevent-place/main/examples/demo-overlay.png" ); //loadOverlay("https://raw.githubusercontent.com/ludolpif/overlay-zevent-place/main/examples/demo-overlay2.png" ); //loadOverlay("https://somewebsite.com/someoverlay.png","A short title" ); /* * EN: Script users: you can edit loadOverlay(...) lines above to memorize in your browser * your overlay choices without using the "Overlays" menu from this script on https://place.zevent.fr/ * To do that: * 0) Make sure you read this from a web browser's tab, from the TamperMonkey extension * (if not, you have missed steps in the documentation below README.md: https://github.com/ludolpif/overlay-zevent-place ) * 1) Use an line of code left as example like //loadOveray(...); * 2) SECURITY: don't try to load anything but a .png file * 3) Replace the example URL by the URL the the overlay of your choice * 4) Remove the double-slash // before loadOverlay(...); to enable this line of code * 5) Save the script (Ctrl+S) * 6) Close this tab (Tampermonkey editor) * 7) Go on https://place.zevent.fr browser tab and refresh the page with Ctrl+R * Remarks : * - Overlays will display only after successful authentication on https://place.zevent.fr website * - Don't mess up any semi-colon (;) at end of code lines, it will break the script. * Safe mode : * - If you have a bug with the UI, put your URLs in some loadOverlay(...); lines * and remove the double-slash // before //safeModeDisableUI... line */ function loadOverlay(url, title, id) { zpoLog("loadOverlay(" + url + ", " + title + ", " + id + ")"); const checkedURL = urlSanityCheck(url); let checkedTitle = textSanityFilter(title); if (checkedTitle === '(invalid)') { checkedTitle = checkedURL.replace(/^.*\/([^%?<>&]+)$/, '$1'); } let checkedId = idSanityCheck(id); if (checkedId === false) { checkedId = "custom-" + lastCustomId++; } wantedOverlays[checkedId] = { id:checkedId, url: checkedURL, title:checkedTitle }; refreshOverlaysState = true; } function reloadOverlays(origCanvas, ourOverlays) { zpoLog("reloadOverlays()"); const parentDiv = origCanvas.parentElement; // CSS fix for firefox ESR 91 ('pixellated' needs >=93) if (navigator.userAgent.replace(/^Mozilla.* rv:(\d+).*$/, '$1') < 93) { parentDiv.style.setProperty('image-rendering', 'crisp-edges'); } const wantedOverlaysIds = Object.keys(wantedOverlays); // Update wantedOverlays URL from knownOverlays when the wantedOverlay came from there // id could be "custom-N" for manually added URL, it won't match in knownOverlays wantedOverlaysIds.forEach(function (id) { const data = wantedOverlays[id]; if ( knownOverlays[id] && ( typeof knownOverlays[id].overlay_url === "string" ) ) { const newURL = knownOverlays[id].overlay_url; // newURL assumed already sanitized (done when inserted in knownOverlays at first place) if ( newURL.startsWith("https://") && ( data.url != newURL ) ) { zpoLog("reloadOverlays() updating url for id:" + id + " from: " + data.url + "to: " + newURL); data.url = newURL; } } }); // Remove all our // TODO remove all addEventListener before deleting ? if ( !ourOverlays ) ourOverlays = []; ourOverlays.forEach(function (e) { e.remove() }); // Insert them again wantedOverlaysIds.forEach(function (id) { const data = wantedOverlays[id]; appendOverlayInDOM(origCanvas, parentDiv, data.url); }); refreshDisplayTime(document.querySelector('#zevent-place-overlay-wanted-ts')); // Mark job done for keepOurselfInDOM refreshOverlaysState = false; } function reloadUIWantedOverlays() { if (!wantedOverlays) { zpoLog("reloadUIWantedOverlays() for undefined wantedOverlays"); return; } const wantedOverlaysIds = Object.keys(wantedOverlays); zpoLog("reloadUIWantedOverlays() for " + wantedOverlaysIds.length + " wantedOverlays"); // Refresh the list in DOM const ulWantedOverlays = document.querySelector('#zevent-place-overlay-ui-list-wanted-overlays'); if (!ulWantedOverlays) return; ulWantedOverlays.innerHTML = ""; wantedOverlaysIds.forEach(function (id) { appendUIWantedOverlays(ulWantedOverlays, id, wantedOverlays[id]); }); } function reloadUIKnownOverlays() { if (!knownOverlays) { zpoLog("reloadUIKnownOverlays() for undefined knownOverlays"); return; } const knownOverlaysIds = Object.keys(knownOverlays); zpoLog("reloadUIKnownOverlays() for " + knownOverlaysIds.length + " knownOverlays"); // Refresh the list in DOM const ulKnownOverlays = document.querySelector('#zevent-place-overlay-ui-list-known-overlays'); if (!ulKnownOverlays) return; ulKnownOverlays.innerHTML = ""; knownOverlaysIds.forEach(function (id) { appendUIKnownOverlays(ulKnownOverlays, id, knownOverlays[id]); }); } function appendOverlayInDOM(origCanvas, parentDiv, url) { zpoLog("appendOverlayInDOM() url: " + url); const image = document.createElement("img"); image.className = "zevent-place-overlay-img"; // Add ?ts= and a timestamp to skip browser cache, overlays will be hosted at various places, with various Expires: headers image.src = url + "?ts=" + new Date().getTime(); image.style = "background: none; position: absolute; left: 0px; top: 0px;"; image.onload = function (event) { fitOverlayOnCanvas(origCanvas, event.target); } // Append the image on the document, it could change in size a bit later because of image.decode() Promise above parentDiv.appendChild(image); document.addEventListener('keypress', function(event) { if (event.code == 'KeyH') { image.hidden = !image.hidden; } }); } function fitOverlayOnCanvas(origCanvas, image) { zpoLog("fitOverlayOnCanvas()"); const nw = event.target.naturalWidth; const nh = event.target.naturalHeight; if ( !nw || !nh ) { zpoLog("fitOverlayOnCanvas() WARNING: no nw or nh: " + nw + ',' + nh); return; } if ( (nw%300) || (nh%300) ) { zpoLog("fitOverlayOnCanvas() WARNING: adding image size that is not multiple of 300, badly exported overlay"); image.width = origCanvas.width; image.height = origCanvas.height; } else { zpoLog("fitOverlayOnCanvas() nw,nh: " + nw + "," + nh); image.width = nw/3; image.height = nh/3; } zpoLog("fitOverlayOnCanvas() width,height: " + image.width + "," + image.height); } function appendOurUI(origUI) { zpoLog("appendOurUI()"); const ourUI = document.createElement("div"); ourUI.id = "zevent-place-overlay-ui"; ourUI.style = ` padding: 0 8px; border-radius: 20px; background: #1f1f1f; position: fixed; top: 16px; left: 16px; z-index: 999;` ourUI.innerHTML = `
Overlays
`; let btnAdd = ourUI.querySelector('#btn-custom-add'); if (btnAdd) btnAdd.onclick = eventAddCustomOverlay; let btnAskRefreshWantedOverlays = ourUI.querySelector('#btn-refresh-wanted'); if (btnAskRefreshWantedOverlays) btnAskRefreshWantedOverlays.onclick = eventAskRefreshWantedOverlays; let btnAskRefreshKnownOverlays = ourUI.querySelector('#btn-refresh-known'); if (btnAskRefreshKnownOverlays) btnAskRefreshKnownOverlays.onclick = eventAskRefreshKnownOverlays; const versionSpan = ourUI.querySelector('#zevent-place-overlay-ui-version'); if (versionSpan) { versionSpan.innerHTML = 'v' + version }; const nodeUIHead = ourUI.querySelector('#zevent-place-overlay-ui-head'); if (nodeUIHead) { const aScriptUpdate = document.createElement("a"); aScriptUpdate.href = scriptUpdateURL; aScriptUpdate.target = "_blank"; aScriptUpdate.alt = "Aperçu"; aScriptUpdate.innerHTML = ''; nodeUIHead.appendChild(aScriptUpdate); } origUI.appendChild(ourUI); // wantedOverlayURLs may have already values if set with loadOverlay() in script, so display them reloadUIWantedOverlays(); } function appendUIWantedOverlays(ulWantedOverlays, id, data) { zpoLog("appendUIWantedOverlays()"); const tr = document.createElement("tr"); tr.id = 'wanted-node-'+id; tr.style = "padding: 5px"; tr.innerHTML= ` `; let btnDel = tr.querySelector('#btn-del-'+id); if (btnDel) btnDel.onclick = eventDelOverlay; if ( typeof data.title === "string" ) { const nodeTitle = document.createTextNode(data.title); tr.querySelector('.title').appendChild(nodeTitle); } if ( typeof data.url === "string" ) { const aPreview = document.createElement("a"); aPreview.href = data.url; aPreview.target="_blank"; aPreview.alt = "Aperçu"; aPreview.innerHTML = ''; tr.querySelector('.preview_btn').appendChild(aPreview); } ulWantedOverlays.appendChild(tr); } function appendUIKnownOverlays(ulKnownOverlays, id, data) { // Don't concat json data directly in innerHTML (prevent some injection attacks) zpoLog("appendUIKnownOverlays()"); const btnDescriptionClick = "eventToggleKnownOverlayDescription('" + id + "')"; const tr = document.createElement("tr"); tr.id = 'avail-node-'+id; tr.style = "padding: 5px"; tr.innerHTML= ` `; let btnAdd = tr.querySelector('#btn-add-'+id); if (btnAdd) btnAdd.onclick = eventAddKnownOverlay; if ( typeof data.description === "string" ) { let btnDescription = tr.querySelector('#btn-description-'+id); if (btnDescription) btnDescription.onclick = eventToggleKnownOverlayDescription; } if ( typeof data.community_name === "string" ) { const nodeCommunityName = document.createTextNode(data.community_name); tr.querySelector('.community_name').appendChild(nodeCommunityName); } if ( typeof data.community_twitch === "string" ) { const aTwitch = document.createElement("a"); aTwitch.href = data.community_twitch; aTwitch.target="_blank"; aTwitch.alt = "Twitch"; aTwitch.innerHTML = twitchLogoSVG; tr.querySelector('.community_twitch').appendChild(aTwitch); } if ( typeof data.community_discord === "string" ) { const aDiscord= document.createElement("a"); aDiscord.href = data.community_discord; aDiscord.target="_blank"; aDiscord.alt = "Discord"; aDiscord.innerHTML = discordLogoSVG; tr.querySelector('.community_discord').appendChild(aDiscord); } if ( typeof data.thread_url === "string" ) { const aThread = document.createElement("a"); aThread.href = data.thread_url; aThread.target="_blank"; aThread.alt = "Fil"; aThread.innerHTML = 'Fil Discord Commu ZEvent/Place'; tr.querySelector('.thread_url').appendChild(aThread); } ulKnownOverlays.appendChild(tr); const tr2 = document.createElement("tr"); tr2.id = 'desc-node-'+id; tr2.style = "padding: 5px; height: 0px"; tr2.hidden = true; const td2 = document.createElement("td"); td2.colSpan = "6"; td2.style = "padding: 16px;"; tr2.appendChild(td2); if ( typeof data.description === "string" ) { const nodeDescription = document.createTextNode(data.description); td2.appendChild(nodeDescription); } ulKnownOverlays.appendChild(tr2); } function refreshDisplayTime(domNode) { if ( domNode ) { const now = new Date(); domNode.innerHTML = "màj." + now.getHours() + "h" + String(now.getMinutes()).padStart(2, "0"); } } function eventAddKnownOverlay(event) { zpoLog("eventAddKnownOverlay(event)"); let btnId = event.target.id; let id = btnId.replace(/^btn-add-/, ''); const availNode = document.querySelector('#avail-node-' + id); if (availNode) { availNode.style.height = "0px" }; const data = knownOverlays[id]; loadOverlay(data.overlay_url, data.community_name, id); } function eventAddCustomOverlay(event) { zpoLog("eventAddCustomOverlay(event)"); const nodeInput = document.querySelector('#zevent-place-overlay-ui-input-url'); const url = nodeInput.value; loadOverlay(url); } function eventDelOverlay(event) { zpoLog("eventDelOverlay(event)"); let btnId = event.target.id; let id = btnId.replace(/^btn-del-/, ''); delete wantedOverlays[id]; refreshOverlaysState = true; } function eventToggleKnownOverlayDescription(event) { zpoLog("eventToggleKnownOverlayDescription(event)"); let btnId = event.target.id; let id = btnId.replace(/^btn-description-/, ''); const descriptionNode = document.querySelector('#desc-node-' + id); if (descriptionNode) { if ( descriptionNode.hidden ) { descriptionNode.style.height = ''; } else { descriptionNode.style.height = '0px'; } descriptionNode.hidden = !descriptionNode.hidden; } } function eventAskRefreshKnownOverlays(event) { zpoLog("eventAskRefreshKnownOverlays()"); if ( !safeModeDisableGetJSON && refreshKnownOverlaysState == 0) { zpoLog("eventAskRefreshKnownOverlays() refreshKnownOverlaysState before:" + refreshKnownOverlaysState); refreshKnownOverlaysState = 1; zpoLog("eventAskRefreshKnownOverlays() refreshKnownOverlaysState after:" + refreshKnownOverlaysState); } } function eventAskRefreshWantedOverlays(event) { zpoLog("eventAskRefreshWantedOverlays()"); refreshOverlaysState = true; } function appendOurCSS(origHead) { zpoLog("appendOurCSS()"); const style = document.createElement("style"); style.id = 'zevent-place-overlay-css'; style.innerHTML = `/* nothing for now */`; origHead.appendChild(style); } function keepOurselfInDOM() { let origCanvas = document.querySelector('#place-canvas'); if ( !origCanvas ) zpoLog("keepOurselfInDOM() origCanvas: " + origCanvas); let ourOverlays = document.querySelectorAll('.zevent-place-overlay-img'); if ( origCanvas && (!ourOverlays.length || refreshOverlaysState ) ) { // Special skip case skip : if there is no wantedOverlay and no currently displayed overlay if ( !(wantedOverlays && Object.keys(wantedOverlays)==0 && ourOverlays.length == 0) ) { zpoLog("keepOurselfInDOM() origCanvas: " + !!origCanvas + ", ourOverlays: " + ourOverlays.length + ", refreshOverlaysState:" + refreshOverlaysState ); reloadOverlays(origCanvas, ourOverlays); reloadUIWantedOverlays(); } } if ( !safeModeDisableUI ) { /* Nothing for now let origHead = document.querySelector('head'); if ( !origHead ) zpoLog("keepOurselfInDOM() origHead: " + !!origHead); let ourCSS = document.querySelector('#zevent-place-overlay-css'); if ( origHead && !ourCSS ) { zpoLog("keepOurselfInDOM() origHead: " + !!origHead + ", ourCSS: " + !!ourCSS); appendOurCSS(origHead); }*/ let origUI = document.querySelector('.place'); if ( !origUI )zpoLog("keepOurselfInDOM() origUI: " + origUI); let ourUI = document.querySelector('#zevent-place-overlay-ui'); if ( origUI && (!ourUI || ( refreshKnownOverlaysState == 1 ) ) ) { zpoLog("keepOurselfInDOM() origUI: " + !!origUI + ", ourUI: " + !!ourUI); appendOurUI(origUI); if ( safeModeDisableGetJSON ) { reloadUIKnownOverlays(); // With local data (see knownOverlays at bottom of this script) } else { zpoLog("keepOurselfInDOM() refreshKnownOverlaysState before:" + refreshKnownOverlaysState); refreshKnownOverlaysState = 2; zpoLog("keepOurselfInDOM() refreshKnownOverlaysState after:" + refreshKnownOverlaysState); fetchKnownOverlays(); // Will call reloadUIKnownOverlays() when data is ready } } } } function fetchKnownOverlays() { // Try to load json from main url let xmlhttp1 = new XMLHttpRequest(); let xmlhttp2 = new XMLHttpRequest(); // Set a differed try from backup url setTimeout(function () { // If job already done while waiting this backup request to start, don't do anything zpoLog("fetchKnownOverlays() anon() refreshKnownOverlaysState before:" + refreshKnownOverlaysState); if ( refreshKnownOverlaysState != 2 ) { zpoLog("fetchKnownOverlays() finishRefreshKnownOverlaysState not in progress, skipping backup request"); return; } refreshKnownOverlaysState = 3; zpoLog("fetchKnownOverlays() anon() refreshKnownOverlaysState after:" + refreshKnownOverlaysState); xmlhttp2.onreadystatechange = function() { zpoLog("fetchKnownOverlays() xmlhttp2 state: " + this.readyState + " status: " + this.status); if (this.readyState == 4 && this.status == 200 && processJsonResponse(this.responseText)) { finishRefreshKnownOverlays(xmlhttp1); } }; try { xmlhttp2.open("GET", overlayJSON2, true); xmlhttp2.send(); } catch (error) { zpoLog("fetchKnownOverlays() xmlhttp2 Exception"); console.error(error); } }, 1000); // Start the request from the main url xmlhttp1.onreadystatechange = function() { zpoLog("fetchKnownOverlays() xmlhttp1 state: " + this.readyState + " status: " + this.status); if (this.readyState == 4 && this.status == 200 && processJsonResponse(this.responseText)) { finishRefreshKnownOverlays(xmlhttp2); } }; try { xmlhttp1.open("GET", overlayJSON1, true); xmlhttp1.send(); } catch (error) { zpoLog("fetchKnownOverlays() xmlhttp1 Exception"); console.error(error); } } function processJsonResponse(responseText) { zpoLog("processJsonResponse()"); let data, checkedData; try { data = JSON.parse(responseText); checkedData = jsonSanityCheck(data); } catch (error) { zpoLog("processJsonResponse() Exception"); console.error(error); return false; } if (!checkedData ) { zpoLog("processJsonResponse() checkedData is false"); return false; } // If we are here, the data is safety checked and non-empty // Take the new data into account knownOverlays = checkedData; reloadUIKnownOverlays(); refreshDisplayTime(document.querySelector('#zevent-place-overlay-known-ts')); return true; } function jsonSanityCheck(data) { zpoLog("jsonSanityCheck(data)"); let checkedData = {}; if ( typeof data !== "object") return false; const dataIds = Object.keys(data); dataIds.forEach(function (id) { const checkedId = idSanityCheck(id); if ( checkedId === false ) return; const item = data[id]; checkedData[checkedId] = { id: checkedId, community_name: textSanityFilter(item.community_name), community_twitch: urlSanityCheck(item.community_twitch), community_discord: urlSanityCheck(item.community_discord), thread_url: urlSanityCheck(item.thread_url), overlay_url: urlSanityCheck(item.overlay_url), description: textSanityFilter(item.description), } }); return checkedData; } function idSanityCheck(id) { if ( typeof id !== "string" ) return false; let trimmedId = id.replaceAll(/\s/g, ''); if ( !trimmedId.match(/^[A-Za-z0-9-]+$/) ) { zpoLog("idSanityCheck(id) invalid : " + id); return false; } return trimmedId; } function urlSanityCheck(url) { if ( !url ) return null; if ( typeof url !== "string" ) return '#nonstring'; let trimmedURL = url.substring(0,260).replaceAll(/\s/g, ''); if ( !trimmedURL.match(/^https?:\/\/[A-Za-z0-9\/_.-]+$/) ) { zpoLog("urlSanityCheck(url) invalid : " + url); return '#invalid'; } return trimmedURL; } function textSanityFilter(text) { if ( typeof text !== "string" ) return '(invalid)'; return text.substring(0,260).replaceAll(/[^A-Za-z0-9çéèàêùûôÇÉÈÊÀùÛÔ ',;.:*!()?+-]/g, ' '); } function finishRefreshKnownOverlays(xmlhttpToCancel) { // We have successfully got and process the JSON, abort the other xmlhttp if it was running if (xmlhttpToCancel) { xmlhttpToCancel.abort() } // Keep track that we have finished zpoLog("fetchKnownOverlays() refreshKnownOverlaysState before:" + refreshKnownOverlaysState); refreshKnownOverlaysState = 4; zpoLog("fetchKnownOverlays() refreshKnownOverlaysState after:" + refreshKnownOverlaysState); // Refresh wantedOverlay too, in case of URL change refreshOverlaysState = true; // Start a timer to trigger the cooldown end (ratelimiting) setTimeout(function () { if ( refreshKnownOverlaysState == 4 ) { zpoLog("finishRefreshKnownOverlays() anon() refreshKnownOverlaysState before:" + refreshKnownOverlaysState); refreshKnownOverlaysState = 0; zpoLog("finishRefreshKnownOverlays() anon() refreshKnownOverlaysState before:" + refreshKnownOverlaysState); } }, 5000); } // Following embed data to not depend or generate trafic to external webservers const threadLogoB64 = "data:image/png;base64, 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"; const twitchLogoSVG = '' const discordLogoSVG= ''; /* * Following JSON is from URL you can found in global const overlayJSON * It is embed here in case of problems during getting it at runtime. * Use the bot commands on Discord mentionned in @description to publicly register an overlay in this json */ let knownOverlays = {}; // Run the script with delay, MutationObserver fail in some configs (race condition between this script and the original app) //setTimeout(keepOurselfInDOM, 100); let intervalID = setInterval(keepOurselfInDOM, 1000); })();