Rust Twitch Drop bot

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

当前为 2024-08-11 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Rust Twitch Drop bot
// @namespace    http://tampermonkey.net/
// @version      2.5
// @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() {
    try {
      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);
    } catch (error) {
      addLog("inventory-logs-list", `Error getting inventory data: ${error}`);
      refreshPage();
    }
  }

  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();
        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();
      }, 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() {
    try {
      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();
    } catch (error) {
      addLog("online-status-logs-list", `Error updating streamer status: ${error}`);
      refreshPage();
    }
  }

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