Rust Twitch Drop bot

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

Versión del día 11/8/2024. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Rust Twitch Drop bot
// @namespace    http://tampermonkey.net/
// @version      1.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
// @connect      twitch.facepunch.com
// @license      GPLv3

// ==/UserScript==

(function () {
  "use strict";

  const DEFAULT_CONFIG = {
    checkDropsInterval: 10000,
    checkStreamerStatusInterval: 20000,
    updateStreamerOnlineStatusInterval: 20000,
    pageRefreshInterval: 1200000, 
  };

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

  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; }
            #facepunch-frame { position: fixed; top: 0; left: 0; width: 100px; height: 100px; z-index: -9999; opacity: 0; }
            #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="#facepunch-logs-content">Facepunch 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="facepunch-logs-content" class="tab-pane hidden">
                        <p class="text-lg font-bold mb-2">Facepunch Logs:</p>
                        <ul id="facepunch-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 getStreamerNames() {
    return new Promise((resolve) => {
      GM_xmlhttpRequest({
        method: "GET",
        url: "https://twitch.facepunch.com/",
        onload: (response) => {
          const parser = new DOMParser();
          const doc = parser.parseFromString(
            response.responseText,
            "text/html"
          );
          const streamerElements = doc.querySelectorAll(".streamer-name");
          const streamerData = [
            ...Array.from(streamerElements).map((el) => {
              return {
                name: el.textContent.trim().toLowerCase(),
                online:
                  !!el.parentElement.parentElement.querySelector(
                    ".online-status"
                  ),
              };
            }),
            { name: "general", online: false },
          ];
          addLog(
            "facepunch-logs-list",
            `Streamers online status: ${streamerData
              .map((s) => s.name + "(" + s.online + ")")
              .join(", ")}`
          );

          resolve(streamerData);
        },
      });
    });
  }

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

  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() {
    const streamerData = await getStreamerNames();
    streamers = streamerData.map(({ name, online }) => ({
      name,
      online,
      allItems: 0,
      itemNames: [],
      claimedItems: [],
    }));
  }

  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)}`
        );
        campaignData.forEach((data) => {
          const streamerName = data.name.replace(/^\//, "");
          const streamer = streamers.find((s) => s.name === streamerName);
          if (streamer) {
            streamer.allItems = Math.max(streamer.allItems, data.items);
            streamer.itemNames = data.itemNames;
          }
        });
        resolve();
      }, 6000)
    );
  }

  async function checkDropsAndUpdateStreamers() {
    switchToTab("Inventory");
    return new Promise((resolve) =>
      setTimeout(async () => {
        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];

    if (
      !currentStreamer.online ||
      currentStreamer.claimedItems.length === currentStreamer.allItems
    ) {
      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.");
      }
    }
  }

  async function updateStreamerOnlineStatus() {
    const streamerData = await getStreamerNames();
    streamerData.forEach((data) => {
      const streamer = streamers.find((s) => s.name === data.name);
      if (streamer) {
        streamer.online = data.online;
      }
    });
    updateInfoPanel();
  }

  async function refreshPage() {
    location.reload();
  }

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

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