Greasy Fork is available in English.

Rust Twitch Drop bot

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}`
    );
  }
})();