您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Twitch Auto Claim, Drop, change channel and auto track progress
// ==UserScript== // @name Rust Twitch Drop bot // @namespace http://tampermonkey.net/ // @version 2.7 // @description Twitch Auto Claim, Drop, change channel and auto track progress // @author gig4d3v // @match https://www.twitch.tv/drops/inventory // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_addElement // @require https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js // @require https://code.jquery.com/jquery-3.6.0.min.js // @require https://code.jquery.com/ui/1.12.1/jquery-ui.min.js // @license GPLv3 // ==/UserScript== (function () { "use strict"; const DEFAULT_CONFIG = { checkDropsInterval: 60000, checkStreamerStatusInterval: 20000, updateStreamerOnlineStatusInterval: 20000, pageRefreshInterval: 3600000, watchdogInterval: 60000, retryInterval: 2000, maxRetries: 5, elementTimeout: 5000, tabSwitchDelay: 5000, refreshCooldown: 300000, selectedCampaign: null, availableCampaigns: [], muteIframe: "true", iframeQuality: "low", }; const CONFIG = JSON.parse(localStorage.getItem("twitchDropsManagerConfig")) || DEFAULT_CONFIG; let allOnlineStreamersHaveAllItems = false; let streamers = []; let currentStreamerIndex = 0; let lastActivityTimestamp = Date.now(); let startTime = Date.now(); let consecutiveRefreshCount = 0; const maxConsecutiveRefreshes = 5; let initialDataLoaded = false; let lastRefreshTimestamp = 0; let actionPill; const StateHelper = { state: new Set(), setState(action) { this.state.add(action); this.updateActionPill(); }, clearState(action) { this.state.delete(action); this.updateActionPill(); }, updateActionPill() { const states = { initializing: "Initializing...", fetching: "Fetching Data...", streaming: "Streaming...", checkingDrops: "Checking Drops...", updatingStatus: "Updating Status...", refreshing: "Refreshing Page...", error: "Error", idle: "Idle", }; const activeStates = Array.from(this.state) .map((state) => states[state] || state) .join(" | ") || "Idle"; actionPill.text(activeStates); }, }; function saveConfig() { localStorage.setItem("twitchDropsManagerConfig", JSON.stringify(CONFIG)); } function resetToDefaults() { Object.assign(CONFIG, DEFAULT_CONFIG); saveConfig(); refreshPage(); } function saveLog(message) { const logs = JSON.parse(localStorage.getItem("twitchDropsLogs")) || []; const timestamp = new Date().toISOString(); logs.push({ timestamp, message }); if (logs.length > 100) { logs.shift(); } localStorage.setItem("twitchDropsLogs", JSON.stringify(logs)); } function applyStyles() { GM_addStyle(` @import url('https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css'); .tab-content::-webkit-scrollbar { width: 5px !important; } .tab-content::-webkit-scrollbar-track { background-color: #ebebeb !important; -webkit-border-radius: 10px !important; border-radius: 10px !important; } .tab-content::-webkit-scrollbar-thumb { -webkit-border-radius: 10px !important; border-radius: 10px !important; background: #6d6d6d !important; } `); } function createComponent(tag, className, content, attrs = {}) { const element = $( `<${tag} class="${className} custom-scrollbar">${content}</${tag}>` ); for (const [key, value] of Object.entries(attrs)) { element.attr(key, value); } return element; } function centerPopup(popup) { const winWidth = $(window).width(); const winHeight = $(window).height(); const popupWidth = popup.outerWidth(); const popupHeight = popup.outerHeight(); const left = (winWidth - popupWidth) / 2; const top = (winHeight - popupHeight) / 2; popup.css({ left: `${left}px`, top: `${top}px` }); } function createPopup(id, title, content) { const popup = createComponent( "div", "fixed z-50 bg-gray-800 text-white rounded-lg shadow-lg custom-scrollbar", "", { id, style: "display: none; min-width: 300px; position: fixed !important;", } ); const header = createComponent( "div", "popup-header bg-gray-900 p-2 rounded-t-lg flex justify-between items-center cursor-move", ` <span class="text-xl font-bold">${title}</span> <button class="close-popup bg-red-600 text-white px-2 rounded">X</button> ` ); const body = createComponent("div", "popup-content p-2", content); popup.append(header).append(body); $("body").append(popup); popup.draggable({ handle: ".popup-header" }).resizable(); header.find(".close-popup").on("click", () => popup.hide()); centerPopup(popup); return popup; } function createMainPopup() { const content = ` <ul class="tabs flex space-x-2"> <li class="tab active p-2 cursor-pointer bg-gray-800 rounded-t-lg" data-target="#streamer-list-content">Streamer List</li> <li class="tab p-2 cursor-pointer bg-gray-800 rounded-t-lg" data-target="#inventory-logs-content">Inventory Logs</li> <li class="tab p-2 cursor-pointer bg-gray-800 rounded-t-lg" data-target="#online-status-logs-content">Online Status Logs</li> <li class="tab p-2 cursor-pointer bg-gray-800 rounded-t-lg" data-target="#streamer-logs-content">Streamer Logs</li> <li class="tab p-2 cursor-pointer bg-gray-800 rounded-t-lg" data-target="#global-logs-content">Global Logs</li> <li class="tab p-2 cursor-pointer bg-gray-800 rounded-t-lg" data-target="#config-content">Config</li> </ul> <div class="tab-content p-4 bg-gray-800 rounded-b-lg text-lg overflow-y-scroll custom-scrollbar" style='width: 700px; height: 340px;'> <div id="streamer-list-content" class="tab-pane active"> <p class="text-lg font-bold mb-2">Current Streamer: <span id="current-streamer" class="font-normal"></span></p> <ul id="streamer-list" class="list-disc pl-5 space-y-1 custom-scrollbar"></ul> </div> <div id="inventory-logs-content" class="tab-pane hidden"> <p class="text-lg font-bold mb-2">Inventory Logs:</p> <ul id="inventory-logs-list" class="list-disc pl-5 space-y-1 custom-scrollbar"></ul> </div> <div id="online-status-logs-content" class="tab-pane hidden"> <p class="text-lg font-bold mb-2">Online Status Logs:</p> <ul id="online-status-logs-list" class="list-disc pl-5 space-y-1 custom-scrollbar"></ul> </div> <div id="streamer-logs-content" class="tab-pane hidden"> <p class="text-lg font-bold mb-2">Streamer Logs:</p> <ul id="streamer-logs-list" class="list-disc pl-5 space-y-1 custom-scrollbar"></ul> </div> <div id="global-logs-content" class="tab-pane hidden"> <p class="text-lg font-bold mb-2">Global Logs:</p> <ul id="global-logs-list" class="list-disc pl-5 space-y-1 custom-scrollbar"></ul> </div> <div id="config-content" class="tab-pane hidden"> <p class="text-lg font-bold mb-2">Configuration:</p> <label class="block mb-2"> <span>Check Drops Interval (ms):</span> <input type="number" id="check-drops-interval" class="bg-gray-700 text-white p-2 rounded w-full" value="${ CONFIG.checkDropsInterval }"> </label> <label class="block mb-2"> <span>Check Streamer Status Interval (ms):</span> <input type="number" id="check-streamer-status-interval" class="bg-gray-700 text-white p-2 rounded w-full" value="${ CONFIG.checkStreamerStatusInterval }"> </label> <label class="block mb-2"> <span>Update Streamer Online Status Interval (ms):</span> <input type="number" id="update-streamer-online-status-interval" class="bg-gray-700 text-white p-2 rounded w-full" value="${ CONFIG.updateStreamerOnlineStatusInterval }"> </label> <label class="block mb-2"> <span>Page Refresh Interval (ms):</span> <input type="number" id="page-refresh-interval" class="bg-gray-700 text-white p-2 rounded w-full" value="${ CONFIG.pageRefreshInterval }"> </label> <label class="block mb-2"> <span>Watchdog Timer Interval (ms):</span> <input type="number" id="watchdog-interval" class="bg-gray-700 text-white p-2 rounded w-full" value="${ CONFIG.watchdogInterval }"> </label> <label class="block mb-2"> <span>Element Timeout (ms):</span> <input type="number" id="element-timeout" class="bg-gray-700 text-white p-2 rounded w-full" value="${ CONFIG.elementTimeout }"> </label> <label class="block mb-2"> <span>Tab Switch Delay (ms):</span> <input type="number" id="tab-switch-delay" class="bg-gray-700 text-white p-2 rounded w-full" value="${ CONFIG.tabSwitchDelay }"> </label> <label class="block mb-2"> <span>Refresh Cooldown (ms):</span> <input type="number" id="refresh-cooldown" class="bg-gray-700 text-white p-2 rounded w-full" value="${ CONFIG.refreshCooldown }"> </label> <label class="block mb-2"> <span>Mute Iframe:</span> <select id="mute-iframe" class="bg-gray-700 text-white p-2 rounded w-full"> <option value="true" ${ CONFIG.muteIframe === "true" ? "selected" : "" }>Yes</option> <option value="false" ${ CONFIG.muteIframe === "false" ? "selected" : "" }>No</option> </select> </label> <label class="block mb-2"> <span>Iframe Quality:</span> <select id="iframe-quality" class="bg-gray-700 text-white p-2 rounded w-full"> <option value="low" ${ CONFIG.iframeQuality === "low" ? "selected" : "" }>Low</option> <option value="medium" ${ CONFIG.iframeQuality === "medium" ? "selected" : "" }>Medium</option> <option value="high" ${ CONFIG.iframeQuality === "high" ? "selected" : "" }>High</option> </select> </label> <label class="block mb-2"> <span>Selected Campaign:</span> <select id="selected-campaign" class="bg-gray-700 text-white p-2 rounded w-full"> ${CONFIG.availableCampaigns .map( (campaign) => `<option value="${campaign}" ${ CONFIG.selectedCampaign === campaign ? "selected" : "" }>${campaign}</option>` ) .join("")} </select> </label> <button id="save-config" class="bg-green-600 text-white px-4 py-2 rounded">Save</button> <button id="reset-config" class="bg-red-600 text-white px-4 py-2 rounded mt-2">Reset to Defaults</button> </div> </div> `; return createPopup("info-popup", "Twitch Drops Manager", content); } function createCampaignSelectionPopup() { const content = ` <select id="campaign-select" class="bg-gray-700 text-white p-2 rounded w-full"></select> <button id="select-campaign-button" class="bg-green-600 text-white px-4 py-2 rounded mt-2 w-full">Select</button> `; return createPopup( "campaign-selection-popup", "Select a Campaign", content ); } function createStreamerFrame() { const container = createComponent( "div", "fixed z-50 bg-gray-800 rounded-lg shadow-lg draggable resizable custom-scrollbar", "", { id: "streamer-frame-container", style: "display: none; width: fit-content; height: fit-content; position: fixed !important;", } ); const header = createComponent( "div", "popup-header bg-gray-900 p-2 rounded-t-lg flex justify-between items-center cursor-move", ` <span class="text-xl font-bold" id="streamer-title">Streamer Window</span> <button id="minimize-streamer" class="bg-blue-600 text-white px-2 rounded">-</button> ` ); const iframe = createComponent("iframe", "", "", { id: "streamer-frame", src: "https://www.kcchanphotography.com/resources/website/common/images/loading-spin.svg", style: "width: 700px; height: 500px", }); container.append(header).append(iframe); $("body").append(container); container.draggable({ handle: ".popup-header" }).resizable(); header.find("#minimize-streamer").on("click", () => { const minimized = container.hasClass("minimized"); container.toggleClass("minimized", !minimized); iframe.toggle(minimized); header.find("#minimize-streamer").text(minimized ? "+" : "-"); }); centerPopup(container); return container; } function createActionButton() { const button = createComponent( "button", "fixed bottom-4 right-4 bg-blue-600 text-white p-2 rounded shadow-lg z-50 custom-scrollbar", "Twitch Drops Manager" ); actionPill = createComponent( "span", "fixed bottom-4 left-4 bg-gray-700 text-white p-2 rounded shadow-lg z-50 custom-scrollbar", "Initializing..." ); button.on("click", () => $("#info-popup").toggle()); $("body").append(button).append(actionPill); return actionPill; } function addEventListeners() { $(document).on("click", ".tab", function () { $(".tab").removeClass("active"); $(this).addClass("active"); $(".tab-pane").removeClass("active").addClass("hidden"); $($(this).data("target")).removeClass("hidden").addClass("active"); }); $("#save-config").on("click", () => { CONFIG.checkDropsInterval = parseInt( $("#check-drops-interval").val(), 10 ); CONFIG.checkStreamerStatusInterval = parseInt( $("#check-streamer-status-interval").val(), 10 ); CONFIG.updateStreamerOnlineStatusInterval = parseInt( $("#update-streamer-online-status-interval").val(), 10 ); CONFIG.pageRefreshInterval = parseInt( $("#page-refresh-interval").val(), 10 ); CONFIG.watchdogInterval = parseInt($("#watchdog-interval").val(), 10); CONFIG.elementTimeout = parseInt($("#element-timeout").val(), 10); CONFIG.tabSwitchDelay = parseInt($("#tab-switch-delay").val(), 10); CONFIG.refreshCooldown = parseInt($("#refresh-cooldown").val(), 10); CONFIG.muteIframe = $("#mute-iframe").val(); CONFIG.iframeQuality = $("#iframe-quality").val(); CONFIG.selectedCampaign = $("#selected-campaign").val(); saveConfig(); alert("Configuration saved!"); refreshPage(); }); $("#reset-config").on("click", () => { if (confirm("Are you sure you want to reset to defaults?")) { resetToDefaults(); } }); $("#select-campaign-button").on("click", () => { const selectedCampaign = $("#campaign-select").val(); if (selectedCampaign) { CONFIG.selectedCampaign = selectedCampaign; CONFIG.availableCampaigns = $("#campaign-select option") .map((_, option) => option.value) .toArray(); saveConfig(); $("#campaign-selection-popup").hide(); refreshPage(); } }); } function addLog(containerId, message) { const logsListElement = $(`#${containerId}`); const logItem = createComponent("li", "", message); logsListElement.append(logItem); saveLog(message); if (logsListElement.children().length > 100) { logsListElement.children().first().remove(); } } function loadGlobalLogs() { const logs = JSON.parse(localStorage.getItem("twitchDropsLogs")) || []; const logsListElement = $("#global-logs-list"); logsListElement.empty(); logs.forEach((log) => { const logItem = createComponent( "li", "", `${log.timestamp}: ${log.message}` ); logsListElement.append(logItem); }); } function retryFetch( fetchFunction, maxRetries = CONFIG.maxRetries, interval = CONFIG.retryInterval ) { return new Promise((resolve, reject) => { let attempts = 0; const executeFetch = async () => { try { const result = await fetchFunction(); resolve(result); } catch (error) { if (attempts < maxRetries) { attempts++; setTimeout(executeFetch, interval); } else { reject(error); } } }; executeFetch(); }); } function timeoutPromise(ms, promise) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error("Request timed out")); }, ms); promise .then((value) => { clearTimeout(timer); resolve(value); }) .catch((error) => { clearTimeout(timer); reject(error); }); }); } async function getStreamerOnlineStatus(streamerNames) { try { const streamerStatuses = {}; const fetchStatus = (name) => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://www.twitch.tv/${name}`, onload: (response) => { const parser = new DOMParser(); const doc = parser.parseFromString( response.responseText, "text/html" ); const scripts = doc.querySelectorAll("script"); let isLive = false; scripts.forEach((script) => { if (script.textContent.includes("isLiveBroadcast")) { isLive = true; } }); streamerStatuses[name] = isLive; resolve(); }, onerror: () => { streamerStatuses[name] = false; reject(new Error(`Failed to fetch status for ${name}`)); }, }); }); }; await Promise.all( streamerNames.map((name) => retryFetch(() => timeoutPromise(CONFIG.elementTimeout, fetchStatus(name)) ) ) ); return streamerStatuses; } catch (error) { addLog( "online-status-logs-list", `Error fetching streamer statuses: ${error}` ); refreshPage(); throw error; } } async function switchTabs(tabName) { return retryFetch(() => timeoutPromise( CONFIG.elementTimeout, new Promise((resolve, reject) => { try { const tabList = document.querySelectorAll('[role="tablist"]')[0]; if (tabList) { const tabs = tabList.children; for (let i = 0; i < tabs.length; i++) { if (tabs[i].textContent.trim() === tabName) { tabs[i].children[0].click(); setTimeout(resolve, CONFIG.tabSwitchDelay); return; } } } reject(new Error(`Tab ${tabName} not found`)); } catch (error) { reject(error); } }) ) ); } async function getInventoryData() { try { await switchTabs("All Campaigns"); await switchTabs("Inventory"); addLog("inventory-logs-list", "Reloaded inventory progress."); setTimeout(async () => { addLog("inventory-logs-list", "Checking for claim button..."); const claimButton = await getClaimButton(); if (claimButton) { claimButton.click(); addLog("inventory-logs-list", "Claimed a drop."); } }, CONFIG.tabSwitchDelay); } catch (error) { addLog("inventory-logs-list", `Error getting inventory data: ${error}`); refreshPage(); } } function getClaimButton() { return retryFetch(() => timeoutPromise( CONFIG.elementTimeout, new Promise((resolve, reject) => { try { const xpathExpression = "//div[text()='Claim Now']"; const result = document.evaluate( xpathExpression, document, null, XPathResult.ANY_TYPE, null ); const divElement = result.iterateNext(); let grandparentElement = null; if (divElement) { const parentElement = divElement.parentNode; grandparentElement = parentElement.parentNode; resolve(grandparentElement); } else { reject(new Error("Claim button not found")); } } catch (error) { reject(error); } }) ) ); } function getCampaigns() { return retryFetch(() => timeoutPromise( CONFIG.elementTimeout, new Promise((resolve, reject) => { try { const cleanArray = (arr) => { const filteredArray = arr.filter( (item) => item !== null && item !== undefined && !Number.isNaN(item) ); const uniqueArray = [...new Set(filteredArray)]; return uniqueArray; }; const aTags = document.getElementsByTagName("div"); let found = []; const result = []; for (let i = 0; i < aTags.length; i++) { if (aTags[i].textContent === "Watch to Redeem") { found.push( aTags[ i ].parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement.children[0].querySelector( "h3.tw-title" )?.textContent ); } } if (found.length > 0) { resolve(cleanArray(found)); } else { reject(new Error("No campaigns found")); } } catch (error) { reject(error); } }) ) ); } function getCampaignData() { return retryFetch(() => timeoutPromise( CONFIG.elementTimeout, new Promise((resolve, reject) => { try { const aTags = document.getElementsByTagName("h3"); let found; const result = []; for (let i = 0; i < aTags.length; i++) { if (aTags[i].textContent === CONFIG.selectedCampaign) { found = aTags[i]; break; } } if (found) { const mainContainer = found.parentElement.parentElement.parentElement.parentElement .parentElement.parentElement; mainContainer.querySelectorAll("a").forEach((streamer) => { const container = streamer.parentElement.parentElement.parentElement .parentElement.parentElement.parentElement; if ( container.children[0].children[0].textContent === "How to Earn the Drop" ) { const name = streamer.textContent === "a participating live channel" ? "general" : streamer.textContent.toLowerCase(); const items = container.parentElement.children[1].children[1].children[0].querySelectorAll( "img" ).length; const itemNames = []; container.parentElement.children[1].children[1].children[0] .querySelectorAll("img") .forEach((imgEl) => { itemNames.push( imgEl.parentElement.parentElement.parentElement .children[1].children[0].children[0].textContent ); }); result.push({ name, items, itemNames }); } }); } if (result.length > 0) { resolve(result); } else { reject(new Error("No campaigns found")); } } catch (error) { reject(error); } }) ) ); } function getClaimedItemsNamesInv() { return retryFetch(() => timeoutPromise( CONFIG.elementTimeout, new Promise((resolve, reject) => { try { const aTags = document.getElementsByTagName("h5"); let found; const result = []; for (let i = 0; i < aTags.length; i++) { if (aTags[i].textContent === "Claimed") { found = aTags[i]; break; } } if (found) { const itemImgs = found.parentElement.parentElement.parentElement.children[1].querySelectorAll( "img" ); itemImgs.forEach((imgEl) => { result.push( imgEl.parentElement.parentElement.parentElement.children[1] .children[1].children[0].textContent ); }); resolve(result); } else { reject(new Error("Claimed items not found")); } } catch (error) { reject(error); } }) ) ); } function switchToTab(tabName) { return retryFetch(() => timeoutPromise( CONFIG.elementTimeout, new Promise((resolve, reject) => { try { const tabList = document.querySelectorAll('[role="tablist"]')[0]; if (tabList) { const tabs = tabList.children; for (let i = 0; i < tabs.length; i++) { if (tabs[i].textContent.trim() === tabName) { tabs[i].children[0].click(); setTimeout(resolve, CONFIG.tabSwitchDelay); return; } } } reject(new Error(`Tab ${tabName} not found`)); } catch (error) { reject(error); } }) ) ); } async function updateInfoPanel() { return retryFetch(() => timeoutPromise( CONFIG.elementTimeout, new Promise((resolve, reject) => { try { const currentStreamer = streamers[currentStreamerIndex]; if (!currentStreamer) { throw new Error("Current streamer not found"); } $("#current-streamer").text(currentStreamer.name); const streamerListElement = $("#streamer-list"); streamerListElement.empty(); streamers.forEach((streamer) => { const missingItems = streamer.itemNames ? streamer.itemNames.filter( (item) => !streamer.claimedItems.includes(item) ) : []; const listItem = createComponent( "li", "", ` ${streamer.name}: ${streamer.online ? "Online" : "Offline"} - ${ streamer.claimedItems.length }/${streamer.allItems} - Missing Items: ${ missingItems.length ? missingItems.join(", ") : "none" } ` ); streamerListElement.append(listItem); }); const streamerTitle = `${currentStreamer.name} - ${ currentStreamer.online ? "Online" : "Offline" } - ${currentStreamer.claimedItems.length}/${ currentStreamer.allItems }`; $("#streamer-title").text(streamerTitle); resolve(); } catch (error) { reject(error); } }) ) ); } async function initStreamers() { try { await getInitialDataFromCampaigns(); const streamerNames = streamers.map((s) => s.name); const streamerData = await getStreamerOnlineStatus(streamerNames); streamers.forEach((streamer) => { streamer.online = streamerData[streamer.name]; }); initialDataLoaded = true; } catch (error) { addLog("inventory-logs-list", `Error initializing streamers: ${error}`); refreshPage(); } } async function getInitialDataFromCampaigns() { try { await switchToTab("All Campaigns"); return new Promise((resolve) => setTimeout(async () => { try { const campaignData = await getCampaignData(); if ( !campaignData || !Array.isArray(campaignData) || campaignData.length === 0 || campaignData.some( (data) => !data || !data.name || !data.items || !data.itemNames ) ) { addLog( "inventory-logs-list", "Campaign data invalid or empty, refreshing page." ); refreshPage(); return; } addLog( "inventory-logs-list", `Campaign data retrieved: ${JSON.stringify(campaignData)}` ); streamers = campaignData.map((data) => ({ name: data.name.replace(/^\//, ""), online: false, allItems: data.items, itemNames: data.itemNames, claimedItems: [], })); resolve(); } catch (error) { addLog( "inventory-logs-list", `Error getting campaign data: ${error}` ); refreshPage(); } }, CONFIG.tabSwitchDelay) ); } catch (error) { addLog( "inventory-logs-list", `Error getting initial data from campaigns: ${error}` ); refreshPage(); } } async function checkDropsAndUpdateStreamers() { try { StateHelper.setState("checkingDrops"); await switchToTab("Inventory"); return new Promise((resolve) => setTimeout(async () => { try { await getInventoryData(); const claimedItems = await getClaimedItemsNamesInv(); addLog( "inventory-logs-list", `Claimed items retrieved: ${JSON.stringify(claimedItems)}` ); streamers.forEach((streamer) => { streamer.claimedItems = claimedItems.filter((item) => streamer.itemNames.includes(item) ); }); await updateInfoPanel(); resolve(); } catch (error) { addLog( "inventory-logs-list", `Error checking drops and updating streamers: ${error}` ); refreshPage(); } finally { StateHelper.clearState("checkingDrops"); } }, CONFIG.tabSwitchDelay) ); } catch (error) { addLog( "inventory-logs-list", `Error checking drops and updating streamers: ${error}` ); refreshPage(); } } async function checkStreamerStatus() { try { StateHelper.setState("updatingStatus"); const currentStreamer = streamers[currentStreamerIndex]; if (!currentStreamer) { throw new Error("Current streamer not found"); } allOnlineStreamersHaveAllItems = streamers .filter((s) => s.online) .every((s) => s.allItems === s.claimedItems.length); if ( (!currentStreamer.online || currentStreamer.claimedItems.length === currentStreamer.allItems) && !allOnlineStreamersHaveAllItems ) { let nextStreamerFound = false; for (let i = 0; i < streamers.length; i++) { currentStreamerIndex = (currentStreamerIndex + 1) % streamers.length; const nextStreamer = streamers[currentStreamerIndex]; if ( nextStreamer && nextStreamer.allItems > nextStreamer.claimedItems.length ) { nextStreamerFound = true; break; } } if (nextStreamerFound) { await updateInfoPanel(); setIframeSrc(streamers[currentStreamerIndex].name); addLog( "streamer-logs-list", `Switched to next streamer: ${streamers[currentStreamerIndex].name}` ); } else { addLog( "streamer-logs-list", "No more streamers with available drops." ); } } else if (!currentStreamer.online && allOnlineStreamersHaveAllItems) { addLog("streamer-logs-list", "All streamers have all items."); let nextStreamerFound = false; for (let i = 0; i < streamers.length; i++) { currentStreamerIndex = Math.floor(Math.random() * streamers.length); const nextStreamer = streamers[currentStreamerIndex]; if (nextStreamer && nextStreamer.online) { nextStreamerFound = true; break; } } if (nextStreamerFound && currentStreamer != nextStreamerFound) { await updateInfoPanel(); setIframeSrc(streamers[currentStreamerIndex].name); addLog( "streamer-logs-list", `Switched to random online streamer: ${streamers[currentStreamerIndex].name}` ); } else { addLog("streamer-logs-list", "No more online streamers."); } } else { addLog("streamer-logs-list", "No need to change streamer"); if ( $("#streamer-frame").attr("src") === "https://www.kcchanphotography.com/resources/website/common/images/loading-spin.svg" ) { setIframeSrc(currentStreamer.name); } } } catch (error) { addLog("streamer-logs-list", `Error checking streamer status: ${error}`); refreshPage(); } finally { StateHelper.clearState("updatingStatus"); } } async function updateStreamerOnlineStatus() { try { StateHelper.setState("updatingStatus"); const streamerNames = streamers.map((s) => s.name); const streamerData = await getStreamerOnlineStatus(streamerNames); streamers.forEach((streamer) => { streamer.online = streamerData[streamer.name]; addLog( "online-status-logs-list", `${streamer.name} is ${streamer.online ? "online" : "offline"}` ); }); await updateInfoPanel(); } catch (error) { addLog( "online-status-logs-list", `Error updating streamer status: ${error}` ); refreshPage(); } finally { StateHelper.clearState("updatingStatus"); } } async function refreshPage() { const now = Date.now(); if (now - lastRefreshTimestamp > CONFIG.refreshCooldown) { StateHelper.setState("refreshing"); window.location.href = "https://www.twitch.tv/drops/inventory"; } else { addLog( "inventory-logs-list", "Refresh cooldown in effect, skipping refresh." ); } } async function promptForCampaignChoice() { await switchToTab("All Campaigns"); setTimeout(async () => { const campaigns = await getCampaigns(); const campaignSelect = $("#campaign-select"); campaignSelect.empty(); campaigns.forEach((campaign) => { const option = createComponent("option", "", campaign, { value: campaign, }); campaignSelect.append(option); }); CONFIG.availableCampaigns = campaigns; saveConfig(); $("#campaign-selection-popup").show(); }, CONFIG.tabSwitchDelay); } async function main() { try { StateHelper.setState("initializing"); if (!CONFIG.selectedCampaign) { await promptForCampaignChoice(); } else { StateHelper.setState("fetching"); $("#streamer-frame-container").show(); await initStreamers(); await checkDropsAndUpdateStreamers(); await checkStreamerStatus(); StateHelper.setState("streaming"); setInterval(checkDropsAndUpdateStreamers, CONFIG.checkDropsInterval); setInterval(checkStreamerStatus, CONFIG.checkStreamerStatusInterval); setInterval( updateStreamerOnlineStatus, CONFIG.updateStreamerOnlineStatusInterval ); setInterval(refreshPage, CONFIG.pageRefreshInterval); setInterval(watchdogTimer, CONFIG.watchdogInterval); setInterval(updateRunningTime, 1000); } } catch (error) { StateHelper.setState("error"); addLog("inventory-logs-list", `Error in main function: ${error}`); refreshPage(); } finally { StateHelper.clearState("initializing"); StateHelper.clearState("fetching"); } } function watchdogTimer() { const currentTime = Date.now(); if (currentTime - lastActivityTimestamp > CONFIG.watchdogInterval * 2) { if (initialDataLoaded) { if (consecutiveRefreshCount < maxConsecutiveRefreshes) { addLog( "inventory-logs-list", "Watchdog timer triggered, refreshing page." ); consecutiveRefreshCount++; refreshPage(); } else { addLog( "inventory-logs-list", "Max consecutive refreshes reached. Pausing refreshes." ); setTimeout(() => { consecutiveRefreshCount = 0; }, CONFIG.refreshCooldown); } } else { addLog( "inventory-logs-list", "Watchdog timer check: waiting for initial data to load." ); } } else { addLog( "inventory-logs-list", "Watchdog timer check: script is running fine." ); consecutiveRefreshCount = 0; } } function updateLastActivity() { lastActivityTimestamp = Date.now(); addLog("inventory-logs-list", "Heartbeat log: script is running."); } function updateRunningTime() { const currentTime = Date.now(); const elapsedTime = currentTime - startTime; const hours = Math.floor(elapsedTime / 3600000); const minutes = Math.floor((elapsedTime % 3600000) / 60000); const seconds = Math.floor((elapsedTime % 60000) / 1000); $("#bot-running-time").text(`${hours}h ${minutes}m ${seconds}s`); } const originalSetInterval = setInterval; const originalSetTimeout = setTimeout; window.setInterval = function (callback, interval) { const wrappedCallback = function () { updateLastActivity(); callback(); }; return originalSetInterval(wrappedCallback, interval); }; window.setTimeout = function (callback, timeout) { const wrappedCallback = function () { updateLastActivity(); callback(); }; return originalSetTimeout(wrappedCallback, timeout); }; window.addEventListener("load", () => { applyStyles(); createStreamerFrame(); createMainPopup(); createCampaignSelectionPopup(); actionPill = createActionButton(); loadGlobalLogs(); addEventListeners(); setTimeout(main, 10000); }); function setIframeSrc(streamerName) { const currentStreamer = streamers[currentStreamerIndex]; const streamerTitle = `${currentStreamer.name} - ${ currentStreamer.online ? "Online" : "Offline" } - ${currentStreamer.claimedItems.length}/${currentStreamer.allItems}`; $("#streamer-title").text(streamerTitle); $("#streamer-frame").attr( "src", `https://player.twitch.tv/?channel=${streamerName}&parent=www.twitch.tv&muted=${ CONFIG.muteIframe === "true" }&quality=${CONFIG.iframeQuality}` ); } })();