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