Rust Twitch Drop bot

Twitch Auto Claim, Drop, change channel and auto track progress

目前为 2024-08-11 提交的版本。查看 最新版本

// ==UserScript==
// @name         Rust Twitch Drop bot
// @namespace    http://tampermonkey.net/
// @version      2.3
// @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: 10000,
    checkStreamerStatusInterval: 20000,
    updateStreamerOnlineStatusInterval: 20000,
    pageRefreshInterval: 3600000,
  };

  const CONFIG =
    JSON.parse(localStorage.getItem("twitchDropsManagerConfig")) ||
    DEFAULT_CONFIG;

  let allOnlineStreamersHaveAllItems = false;
  let streamers = [];
  let currentStreamerIndex = 0;

  function saveConfig() {
    localStorage.setItem("twitchDropsManagerConfig", JSON.stringify(CONFIG));
  }

  function applyStyles() {
    GM_addStyle(`
            @import url('https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css');
            .draggable { z-index: 9999; }
            .popup-header { cursor: move; }
            .hidden { display: none; }
            .tabs { border-bottom: 1px solid #ccc; }
            .tabs li { margin-right: 1rem; padding-bottom: 0.5rem; cursor: pointer; }
            .tabs .active { border-bottom: 2px solid #000; }
            .tab-pane { display: none; }
            .tab-pane.active { display: block; }
            #streamer-frame-container { position: fixed; bottom: 100px; left: 100px; width: fit-content; height: fit-content; z-index: 9999; background: #1a202c }
            #streamer-frame { width: 700px; height: 500px; }
            #minimized { height: 40px !important; }
        `);
  }

  function createLayout() {
    const wrapper = document.body;

    const streamerFrameContainer = document.createElement("div");
    streamerFrameContainer.id = "streamer-frame-container";
    streamerFrameContainer.style = "position: fixed !important";
    streamerFrameContainer.className = "draggable resizable";
    streamerFrameContainer.innerHTML = `
            <div id="streamer-header" class="popup-header bg-gray-800 p-2 rounded-t-lg flex justify-between items-center">
                <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>
            </div>
            <iframe id="streamer-frame" src="https://www.kcchanphotography.com/resources/website/common/images/loading-spin.svg"></iframe>
        `;
    wrapper.appendChild(streamerFrameContainer);

    $("#streamer-frame-container")
      .draggable({ handle: ".popup-header" })
      .resizable();

    const openPopupButton = document.createElement("button");
    openPopupButton.innerText = "Open Info Panel";
    openPopupButton.className =
      "fixed bottom-4 right-4 bg-blue-600 text-white p-2 rounded shadow-lg z-50";
    openPopupButton.onclick = openPopup;
    wrapper.appendChild(openPopupButton);

    const popup = document.createElement("div");
    popup.id = "info-popup";
    popup.style = "position: fixed !important";
    popup.className =
      "hidden fixed bg-gray-900 text-white p-4 rounded-lg shadow-lg w-2/5 h-3/5 overflow-auto draggable resizable";
    popup.innerHTML = `
            <div class="popup-header bg-gray-800 p-2 rounded-t-lg flex justify-between items-center">
                <span class="text-xl font-bold">Twitch Drops Manager</span>
                <button id="close-popup" class="bg-red-600 text-white px-2 rounded">X</button>
            </div>
            <div class="popup-content pt-2">
                <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="#config-content">Config</li>
                </ul>
                <div class="tab-content p-4 bg-gray-800 rounded-b-lg text-lg">
                    <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"></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"></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"></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"></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>
                        <button id="save-config" class="bg-green-600 text-white px-4 py-2 rounded">Save</button>
                    </div>
                </div>
            </div>
        `;
    wrapper.appendChild(popup);

    $("#info-popup").draggable({ handle: ".popup-header" }).resizable();
  }

  function addEventListeners() {
    document.getElementById("close-popup").onclick = function () {
      $("#info-popup").addClass("hidden");
    };

    $(document).on("click", ".tab", function () {
      $(".tab").removeClass("active");
      $(this).addClass("active");
      $(".tab-pane").removeClass("active").addClass("hidden");
      $($(this).data("target")).removeClass("hidden").addClass("active");
    });

    document.getElementById("minimize-streamer").onclick = function () {
      const streamerContainer = document.getElementById(
        "streamer-frame-container"
      );
      const streamerFrame = document.getElementById("streamer-frame");
      if (streamerContainer.classList.contains("minimized")) {
        streamerContainer.classList.remove("minimized");
        streamerFrame.style.display = "block";
        this.innerText = "-";
      } else {
        streamerContainer.classList.add("minimized");
        streamerFrame.style.display = "none";
        this.innerText = "+";
      }
    };

    document.getElementById("save-config").onclick = function () {
      CONFIG.checkDropsInterval = parseInt(
        document.getElementById("check-drops-interval").value,
        10
      );
      CONFIG.checkStreamerStatusInterval = parseInt(
        document.getElementById("check-streamer-status-interval").value,
        10
      );
      CONFIG.updateStreamerOnlineStatusInterval = parseInt(
        document.getElementById("update-streamer-online-status-interval").value,
        10
      );
      CONFIG.pageRefreshInterval = parseInt(
        document.getElementById("page-refresh-interval").value,
        10
      );
      saveConfig();
      alert("Configuration saved!");
    };
  }

  function openPopup() {
    $("#info-popup").removeClass("hidden");
  }

  function addLog(containerId, message) {
    const logsListElement = document.getElementById(containerId);
    const logItem = document.createElement("li");
    logItem.innerText = message;
    logsListElement.appendChild(logItem);
  }

  function getStreamerOnlineStatus(streamerNames) {
    return new Promise((resolve) => {
        const streamerStatuses = {};

        const fetchStatus = (name) => {
            return new Promise((resolve) => {
                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;
                        resolve();
                    }
                });
            });
        };

        const promises = streamerNames.map(fetchStatus);

        Promise.all(promises)
            .then(() => resolve(streamerStatuses));
    });
  }

  function switchTabs(tabName) {
    return new Promise((resolve) => {
      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();
            break;
          }
        }
      }
      setTimeout(resolve, 1000);
    });
  }

  async function getInventoryData() {
    await switchTabs("All Campaigns");
    await switchTabs("Inventory");
      addLog("inventory-logs-list", "Reloaded inventory progress.");

      setTimeout(() => {
          addLog("inventory-logs-list", "Checking for claim button...");
    const claimButton = getClaimButton();
    if (claimButton) {
      claimButton.click();
      addLog("inventory-logs-list", "Claimed a drop.");
    }
   }, 1000)
  }

  function getClaimButton() {
    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;
    }

    return grandparentElement;
  }

  function getCampaignData() {
    const aTags = document.getElementsByTagName("h3");
    let found;
    const result = [];

    for (let i = 0; i < aTags.length; i++) {
      if (aTags[i].textContent === "Rust") {
        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 });
        }
      });
    }

    return result;
  }

  function getClaimedItemsNamesInv() {
    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
        );
      });
    }

    return result;
  }

  function switchToTab(tabName) {
    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();
          break;
        }
      }
    }
  }

  function updateInfoPanel() {
    const currentStreamer = streamers[currentStreamerIndex];
    document.getElementById("current-streamer").innerText =
      currentStreamer.name;
    const streamerListElement = document.getElementById("streamer-list");
    streamerListElement.innerHTML = "";
    streamers.forEach((streamer) => {
      const listItem = document.createElement("li");
      const missingItems = streamer.itemNames
        ? streamer.itemNames.filter(
            (item) => !streamer.claimedItems.includes(item)
          )
        : [];
      listItem.innerText = `${streamer.name}: ${
        streamer.online ? "Online" : "Offline"
      } - ${streamer.claimedItems.length}/${
        streamer.allItems
      } - Missing Items: ${
        missingItems.length ? missingItems.join(", ") : "none"
      }`;
      streamerListElement.appendChild(listItem);
    });

    const streamerTitle = `${currentStreamer.name} - ${
      currentStreamer.online ? "Online" : "Offline"
    } - ${currentStreamer.claimedItems.length}/${currentStreamer.allItems}`;
    document.getElementById("streamer-title").innerText = streamerTitle;
  }

  async function initStreamers() {
    await getInitialDataFromCampaigns();
    const streamerNames = streamers.map(s => s.name);
    const streamerData = await getStreamerOnlineStatus(streamerNames);
    streamers.forEach(streamer => {
      streamer.online = streamerData[streamer.name];
    });
  }

  async function getInitialDataFromCampaigns() {
    switchToTab("All Campaigns");
    return new Promise((resolve) =>
      setTimeout(async () => {
        const campaignData = getCampaignData();
        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();
      }, 6000)
    );
  }

  async function checkDropsAndUpdateStreamers() {
    switchToTab("Inventory");
    return new Promise((resolve) =>
      setTimeout(async () => {
        await getInventoryData();
        const claimedItems = getClaimedItemsNamesInv();
        addLog(
          "inventory-logs-list",
          `Claimed items retrieved: ${JSON.stringify(claimedItems)}`
        );
        streamers.forEach((streamer) => {
          streamer.claimedItems = claimedItems.filter((item) =>
            streamer.itemNames.includes(item)
          );
        });

        updateInfoPanel();
        resolve();
      }, 1000)
    );
  }

  async function checkStreamerStatus() {
    const currentStreamer = streamers[currentStreamerIndex];
    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.allItems > nextStreamer.claimedItems.length) {
          nextStreamerFound = true;
          break;
        }
      }
      if (nextStreamerFound) {
        updateInfoPanel();
        document.getElementById(
          "streamer-frame"
        ).src = `https://www.twitch.tv/${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.online) {
            nextStreamerFound = true;
            break;
          }
        }
        if (nextStreamerFound && currentStreamer != nextStreamerFound) {
          updateInfoPanel();
          document.getElementById(
            "streamer-frame"
          ).src = `https://www.twitch.tv/${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 (document.getElementById("streamer-frame").src === "https://www.kcchanphotography.com/resources/website/common/images/loading-spin.svg") {
                document.getElementById("streamer-frame").src = `https://www.twitch.tv/${currentStreamer.name}`;
            }
      }
  }

  async function updateStreamerOnlineStatus() {
    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'}`);
    });
    updateInfoPanel();
  }

  async function refreshPage() {
    window.location.href = "https://www.twitch.tv/drops/inventory";
  }

  async function main() {
    return new Promise((resolve) =>
      setTimeout(async () => {
        await initStreamers();
        await checkDropsAndUpdateStreamers();
        checkStreamerStatus();
        setInterval(checkDropsAndUpdateStreamers, CONFIG.checkDropsInterval);
        setInterval(checkStreamerStatus, CONFIG.checkStreamerStatusInterval);
        setInterval(
          updateStreamerOnlineStatus,
          CONFIG.updateStreamerOnlineStatusInterval
        );
        setInterval(refreshPage, CONFIG.pageRefreshInterval);
      }, 6000)
    );
  }

  applyStyles();
  createLayout();
  addEventListeners();
  main();
})();

QingJ © 2025

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