async function main() {
await init_gm_config();
await wait_for_gm_config();
if(GM_config.get("script_enabled")) {
const custom_css = GM_config.get("custom_css_styles")?.trim();
if (custom_css)
GM_addStyle(custom_css);
if(GM_config.get("hide_powerups"))
hide_powerups();
if(GM_config.get("collect_point_bonus"))
collect_point_bonus();
wait_for_element(".chat-input").then(async () => {
if(GM_config.get("irc"))
if(GM_config.get("auth_username") != "" && GM_config.get("auth_oauth") != "")
connect_to_twitch();
else
Swal.fire({
title: "Missing IRC Credentials!",
text: "IRC is selected, but your username or OAuth token is missing or invalid. Please update your settings or disable \"Use IRC\".",
icon: "error",
theme: "dark",
backdrop: false
});
if(GM_config.get("notifications"))
observe_chat_for_username_mentions();
wait_for_element(".community-points-summary").then(async () => {
if(GM_config.get("voucher_buttons"))
generate_voucher_buttons();
});
if(GM_config.get("show_streamelements_points"))
show_streamelements_points();
insert_command_buttons(generate_button_groups());
});
}
}
// ========================
// Username
// ========================
let username = get_username_from_cookie();
function get_username_from_cookie() {
// Get all cookies
const cookies = document.cookie.split(";");
// Search for the "name" cookie
for (const cookie of cookies) {
const [cookie_name, cookie_value] = cookie.trim().split("=");
if (cookie_name === "name")
return decodeURIComponent(cookie_value); // URL-decode the value
}
return null; // "name" cookie not found
}
// ========================
// StreamElements Functions & API
// ========================
let se_channel = null; // Variable to store the channel data
let se_user_data = null; // Variable to store user-specific data
let se_user_data_fetch_interval = 1; // In minutes
let se_points_add_delay = 5; // In seconds
let element_se_current = null;
let element_se_change = null;
async function show_streamelements_points() {
// Wait for the channel data to be fetched
await streamelements_fetch_channel_data(streamelements_store);
// Check if the channel data was successfully fetched
if (!se_channel)
return console.error("Channel data could not be fetched.");
if(username)
{
wait_for_element(".k-streamelements_points").then(async () => {
// Initialize the innerHTML with two spans
document.querySelector(".k-streamelements_points").innerHTML = `
StreamElements: <span id="k-se_current_span"></span><span id="k-se_change_span"></span>
`;
// Point to the <span> elements
element_se_current = document.querySelector("#k-se_current_span");
element_se_change = document.querySelector("#k-se_change_span");
await update_se_points(true); // Initial update of points
setInterval(update_se_points, se_user_data_fetch_interval * 60 * 1000);
});
}
}
async function update_se_points(initialization = false) {
// Get current points from the data attribute
const data_points = element_se_current.getAttribute("data-points");
const current = data_points !== null ? parseInt(data_points) : null;
const user_data = await streamelements_fetch_user(se_channel._id, username);
const new_points = user_data?.points ?? null;
if (new_points === null) {
element_se_current.textContent = "N/A";
element_se_current.setAttribute("data-points", null);
element_se_change.textContent = ""; // Clear the change span
return;
}
element_se_current.setAttribute("data-points", new_points);
const diff = new_points - current;
if (diff !== 0 && !initialization) {
// Add class to the change span based on whether the difference is positive or negative
if (diff > 0)
element_se_change.classList.add("k-points_added");
else
element_se_change.classList.add("k-points_subtracted");
// Show the old points in the current span and the difference in the change span
element_se_current.textContent = current;
element_se_change.textContent = ` ${diff >= 0 ? "+" : ""}${diff}`;
await sleep_s(10);
// Use the generic animation method
element_se_change.textContent = "";
await animate_number_counter(element_se_current, current, new_points);
// Remove the class after the animation
element_se_change.classList.remove("k-points_added", "k-points_subtracted");
} else
element_se_current.textContent = new_points;
}
async function streamelements_fetch_channel_data(twitch_channel) {
try {
const response = await fetch(`https://api.streamelements.com/kappa/v2/channels/${twitch_channel}`);
if (!response.ok)
throw new Error(`API request failed with status ${response.status}`);
const data = await response.json();
se_channel = data; // Store the entire JSON response
} catch (error) {
console.error("Error fetching StreamElements channel data:", error);
se_channel = null; // Set to null in case of an error
}
}
async function streamelements_fetch_user(channel_id, username) {
try {
const response = await fetch(`https://api.streamelements.com/kappa/v2/points/${channel_id}/${username}`);
if (!response.ok)
throw new Error(`API request failed with status ${response.status}`);
const data = await response.json();
return data || null; // Return the points or 0 if not found
} catch (error) {
console.error("Error fetching StreamElements points:", error);
return null; // Return null in case of an error
}
}
// ========================
// Twitch React Chat by Cyb3rgamer
// ========================
let current_chat;
function send_message_with_event(message) {
// Update current_chat only if it's undefined or missing the onSendMessage prop
if (!current_chat || !current_chat.props?.onSendMessage)
current_chat = get_current_chat();
// Send the message if current_chat and onSendMessage are available
if (current_chat?.props?.onSendMessage)
current_chat.props.onSendMessage(message);
else
console.error("Current chat is not available or missing onSendMessage prop.");
}
function get_current_chat() {
try {
const chat_node = document.querySelector(`section[data-test-selector="chat-room-component-layout"]`);
if (!chat_node) return null;
// Find the React instance of the chat container
const react_instance = get_react_instance(chat_node);
if (!react_instance) return null;
// Search the React parent nodes for the chat component
const chat_component = search_react_parents(react_instance, (node) => {
return node.stateNode && node.stateNode.props && node.stateNode.props.onSendMessage;
});
return chat_component ? chat_component.stateNode : null;
} catch (error) {
console.error("Error accessing the chat:", error);
return null;
}
}
function get_react_instance(element) {
for (const key in element)
if (key.startsWith("__reactInternalInstance$") || key.startsWith("__reactFiber$"))
return element[key];
return null;
}
function search_react_parents(node, predicate, max_depth = 15, depth = 0) {
if (!node || depth > max_depth) return null;
try {
if (predicate(node))
return node;
} catch (error) {
console.error("Error while searching React parents:", error);
}
return search_react_parents(node.return, predicate, max_depth, depth + 1);
}
// ========================
// Twitch IRC Connection
// ========================
const twitch_host = "irc-ws.chat.twitch.tv";
const twitch_port = 443;
let socket;
let timer;
let reconnect_interval = 5000;
let reconnect_attempts = 0; // Counter for reconnection attempts
const max_reconnect_attempts = 3; // Maximum number of reconnection attempts
function connect_to_twitch() {
socket = new WebSocket(`wss://${twitch_host}:${twitch_port}`);
socket.onopen = () => {
console.log("Twitch connection started.");
// Authenticate and join the channel
socket.send(`PASS ${GM_config.get("auth_oauth").includes("oauth:") ? GM_config.get("auth_oauth") : "oauth:" + GM_config.get("auth_oauth")}`);
socket.send(`NICK ${GM_config.get("auth_username")}`);
socket.send(`JOIN #${twitch_channel}`);
// Start ping timer to prevent disconnect
timer = setInterval(() => {
socket.send("PING :tmi.twitch.tv");
}, 5 * 60 * 1000); // Send a ping every 5 minutes
};
socket.onmessage = (event) => {
const message = event.data;
// Check for authentication failure
if (message.includes("Login authentication failed")) {
console.error("Twitch authentication failed. Please check your username and OAuth token.");
reconnect_attempts++; // Increment the reconnection attempt counter
console.log(`Authentication failed. Attempt ${reconnect_attempts} of ${max_reconnect_attempts}`);
// Close the connection and try to reconnect
socket.close();
return;
}
else if (message.includes("Welcome, GLHF!")) { // Check for successful connection
console.log("Twitch authentication successful.");
reconnect_attempts = 0; // Reset the counter on successful authentication
}
};
socket.onclose = () => {
console.log("Twitch connection closed.");
// Stop the ping timer
clearInterval(timer);
// Check if max reconnection attempts have been reached
if (reconnect_attempts < max_reconnect_attempts) {
reconnect_attempts++; // Increment the reconnection attempt counter
console.log(`Reconnecting... Attempt ${reconnect_attempts} of ${max_reconnect_attempts}`);
// Try to reconnect after a delay
setTimeout(() => connect_to_twitch(), reconnect_interval);
} else
// Show error message if max reconnection attempts are reached
Swal.fire({
title: "Cannot connect to IRC!",
text: "Unable to connect to Twitch with the provided credentials. Please check your username and OAuth token, or disable \"Use IRC\".",
icon: "error",
theme: "dark",
backdrop: false
});
};
socket.onerror = (error) => {
console.error("Twitch connection error:", error);
};
}
function send_message_with_irc(message) {
if (socket.readyState === WebSocket.OPEN)
socket.send(`PRIVMSG #${twitch_channel} :${message}`);
else
console.error("WebSocket is not open. Current state:", socket.readyState);
}
// ========================
// UI and Button Handling
// ========================
function insert_command_buttons(buttongroups) {
let html = `
<div id="k-main-container" class="k-main-container">
<div id="k-streamelements_points" class="k-streamelements_points"></div>
<div id="k-panel-buttons">
<div id="k-make-draggable-button" title="Detach from chat">👆</div>
<div id="k-grab-handle" class="k-hidden">🖐️</div>
<div id="k-pin-button" class="k-hidden" title="Reattach to chat">📌</div>
<div id="k-cart-button" title="Open store">🛒</div>
<div id="k-open-settings" title="Userscript settings">⚙️</div>
</div>
<div id="k-actions" class="k-buttongroups">${buttongroups}</div>
</div>
`;
document.querySelector(".chat-input").insertAdjacentHTML("beforebegin", html);
// Add event listeners for buttons
document.querySelector("#k-targets #k-closebutton")?.addEventListener("click", () => switch_panel(null), false);
document.querySelectorAll(".k-buttongroup .k-actionbutton")?.forEach(el => el.addEventListener("click", generate_command, false));
document.querySelectorAll(".k-buttongroup .k-targetbutton")?.forEach(el => el.addEventListener("click", switch_panel, false));
document.querySelectorAll(".k-selection-label")?.forEach(el => el.addEventListener("click", show_btn_menu, false));
// Draggable buttons
document.querySelector("#k-make-draggable-button")?.addEventListener("mousedown", () => make_draggable());
document.querySelector("#k-pin-button")?.addEventListener("click", () => disable_draggable());
document.querySelector("#k-cart-button")?.addEventListener("click", () => open_store());
document.querySelector("#k-open-settings")?.addEventListener("click", () => GM_config.open());
}
function open_store() {
const store_name = streamelements_store?.trim() || twitch_channel;
const url = `https://streamelements.com/${store_name}/store`;
window.open(url, "_blank");
}
function switch_panel(event) {
document.querySelector("#k-actions").classList.toggle("k-hidden");
document.querySelector("#k-targets").classList.toggle("k-hidden");
if (event) {
const target_count = parseInt(event.target.getAttribute("data-targets"));
const action = event.target.getAttribute("cmd");
const target_buttons_container = document.getElementById("k-targetbuttons");
// Set the data-action attribute for the targets panel
document.querySelector("#k-targets").setAttribute("data-action", action);
// Check if the number of existing buttons matches the target count
const existing_buttons = target_buttons_container.querySelectorAll(".k-actionbutton");
if (existing_buttons.length !== target_count) {
// Clear existing buttons if the count doesn't match
existing_buttons.forEach(button => button.remove());
// Generate new buttons
let target_buttons_html = "";
for (let i = 1; i <= target_count; i++)
target_buttons_html += btngrp_button(i, i);
// Insert new buttons before the close button
target_buttons_container.insertAdjacentHTML("afterbegin", target_buttons_html);
// Add event listeners to the new buttons
target_buttons_container.querySelectorAll(".k-actionbutton").forEach(el => {
el.addEventListener("click", generate_command, false);
});
}
// Adjust CSS for grid layout
target_buttons_container.classList.remove("k-grid-1", "k-grid-2", "k-grid-3", "k-grid-4", "k-grid-5", "k-grid-6", "k-grid-7", "k-grid-8");
// Calculate the number of buttons per row
let buttons_per_row;
if (target_count <= 6)
buttons_per_row = target_count; // 1-6 Buttons: All in one row
else if (target_count === 8)
buttons_per_row = 4; // 8 Buttons: 4 per row
else
buttons_per_row = 6; // 7+ Buttons: 6 per row (except 8)
target_buttons_container.classList.add(`k-grid-${buttons_per_row}`);
}
}
function generate_command(event) {
let cmd = "";
if(event.target.parentNode.parentNode.getAttribute("data-action")) {
cmd = event.target.parentNode.parentNode.getAttribute("data-action"); // Add action attack or devine in case its from the switched panel
// Remove the data and go back to main panel
event.target.parentNode.parentNode.setAttribute("data-action", "");
switch_panel(null);
}
cmd += event.target.getAttribute("cmd");
// Check if the button has random min and max attributes and append a random number if they exist
if (event.target.hasAttribute("data-random-min") && event.target.hasAttribute("data-random-max"))
cmd += `${random_number(parseInt(event.target.getAttribute("data-random-min")), parseInt(event.target.getAttribute("data-random-max")))}`;
let suffix = "!";
cmd = (GM_config.get("prevent_shadowban") ? `${suffix}${randomize_case(cmd)}` : `${suffix}${cmd}`).trim();
if(cmd.trim() !== "" && cmd !== null)
if(GM_config.get("irc"))
send_message_with_irc(cmd);
else
send_message_with_event(cmd);
else
Swal.fire({
icon: "error",
title: "Error",
text: "Please contact script creator, this button doesn't seem to work correctly!",
theme: "dark",
backdrop: false
});
}
function insert_voucher_buttons(html) {
wait_for_element(".chat-input__buttons-container").then(async () => {
html = `<div class="k-store-buttongroups"><div class="k-buttongroup">${html}</div></div>`;
document.querySelector(".chat-input")?.insertAdjacentHTML("afterend", html);
let buttons = document.querySelectorAll(".k-get_voucher_button");
buttons.forEach(button => {
button.addEventListener("click", async event => {
let voucher = event.target.getAttribute("voucher");
let repeats = event.target.getAttribute("data-repeats");
if (repeats !== null && repeats > 1)
await bulk_purchase_product(voucher, parseInt(repeats));
else
await purchase_voucher(event);
}, false);
});
});
}
function generate_voucher_button(voucher, text, options = {}) {
const { classes = "", repeats = null } = options
let base_class = "k-actionbutton k-get_voucher_button"
let combined_classes = (base_class + ` ${classes ?? ""}`).trim()
let attributes = `voucher="${voucher}" class="${combined_classes}"`
if (repeats !== null) attributes += ` data-repeats="${repeats}"`
return `<button ${attributes}>${text}</button>`
}
function btngrp_label(label) {
return `<label class="k-buttongroup-label">${label}</label>`;
}
function lblgrp_label(btn_menu, name, classes="") {
return `<label class="k-selection-label ${classes}" data-btn-menu="${btn_menu}">${name}</label>`;
}
function btngrp_button(cmd, text, options = {}) {
const { classes = "", targets = null, random_min = null, random_max = null } = options;
let base_class = targets !== null ? "k-targetbutton" : "k-actionbutton";
let combined_classes = (base_class + ` ${classes ?? ""}`).trim();
let attributes = `cmd="${cmd}" class="${combined_classes}"`;
if (targets !== null) attributes += ` data-targets="${targets}"`;
if (random_min !== null && random_max !== null) attributes += ` data-random-min="${random_min}" data-random-max="${random_max}"`;
return `<button ${attributes}>${text}</button>`;
}
function show_btn_menu(event) {
let btn_menus = document.querySelectorAll(".k-btn-menu");
let label_group = event.target.closest(".k-labelgroup");
let close_button = label_group.querySelector(`label[data-btn-menu="close"]`);
btn_menus.forEach(el => {
el.getAttribute("data-btn-menu") === event.target.getAttribute("data-btn-menu") ? el.classList.remove("k-hidden") : el.classList.add("k-hidden");
});
let all_hidden = Array.from(btn_menus).every(el => el.classList.contains("k-hidden"));
all_hidden ? close_button.classList.add("k-hidden") : close_button.classList.remove("k-hidden");
}
// ========================
// Draggable Container
// ========================
function make_draggable() {
const container = document.querySelector("#k-main-container");
const make_draggable_button = document.querySelector("#k-make-draggable-button");
const grab_handle = document.querySelector("#k-grab-handle");
const pin_button = document.querySelector("#k-pin-button");
if (container && make_draggable_button && grab_handle && pin_button) {
// Add the "draggable" class
container.classList.add("k-draggable");
// Hide the make-draggable button and show the k-grab-handle and pin button
make_draggable_button.classList.add("k-hidden");
grab_handle.classList.remove("k-hidden");
pin_button.classList.remove("k-hidden");
// Save the initial position of the container relative to the viewport
const initial_rect = container.getBoundingClientRect();
const left = initial_rect.left;
const bottom = initial_rect.bottom;
// Move the container to the body (to ensure it's above other elements)
document.body.appendChild(container);
// Set the initial position using transform
container.style.transform = `translate(${left}px, ${window.innerHeight - bottom}px)`;
// Enable dragging only when the k-grab-handle is clicked
interact(grab_handle).draggable({
listeners: {
move(event) {
const target = container;
const rect = target.getBoundingClientRect();
const window_width = window.innerWidth;
const window_height = window.innerHeight;
// Calculate new position based on mouse movement
let x = rect.left + event.dx;
let y = rect.bottom - container.offsetHeight + event.dy;
// Round x and y to prevent jitter caused by subpixel values
x = Math.round(x);
y = Math.round(y);
// Constrain the position to keep the container within the window bounds
x = Math.max(0, Math.min(x, window_width - rect.width)); // Left and right edges
y = Math.max(50, Math.min(y, window_height - container.offsetHeight)); // Top and bottom edges
// Update the container's position using transform
target.style.transform = `translate(${x}px, ${y}px)`;
}
}
});
}
}
function disable_draggable() {
const container = document.querySelector("#k-main-container");
const make_draggable_button = document.querySelector("#k-make-draggable-button");
const grab_handle = document.querySelector("#k-grab-handle");
const pin_button = document.querySelector("#k-pin-button");
if (container && make_draggable_button && grab_handle && pin_button) {
// Remove the "draggable" class
container.classList.remove("k-draggable");
// Disable dragging
interact(grab_handle).draggable(false);
// Reset the container to its original position
container.style.transform = "translate(0px, 0px)";
container.setAttribute("data-x", 0);
container.setAttribute("data-y", 0);
// Move the container back to the chat panel
document.querySelector(".chat-input")?.insertAdjacentElement("beforebegin", container);
// Show the make-draggable button and hide the k-grab-handle and pin button
make_draggable_button.classList.remove("k-hidden");
grab_handle.classList.add("k-hidden");
pin_button.classList.add("k-hidden");
}
}
// ========================
// Notifications
// ========================
function observe_chat_for_username_mentions() {
wait_for_element(".chat-scrollable-area__message-container").then(async () => {
const chat_container = document.querySelector(".chat-scrollable-area__message-container");
await sleep_s(5);
if(username && username != "") {
// Create a MutationObserver to watch for new messages
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
// Check if the added node is a chat message
let msg = node.querySelector(`span[data-a-target="chat-line-message-body"]`)?.innerText?.trim();
if (msg && msg.toLowerCase().includes(username.toLowerCase())) {
let author = node.querySelector(`.chat-author__display-name`)?.textContent;
GM_notification({
title: `Channel: ${twitch_channel} - ${author} mentioned you!`,
text: `${msg}`,
timeout: 15000,
silent: false
});
}
});
});
});
// Start observing the chat container for new child nodes
observer.observe(chat_container, {
childList: true, // Watch for added or removed child nodes
subtree: true, // Watch all descendants of the container
});
}
});
}
// ========================
// Collect Point Bonus
// ========================
async function collect_point_bonus() {
await wait_for_element(".claimable-bonus__icon").then(async () => {
document.querySelector(".claimable-bonus__icon")?.click();
console.log("BONUS CLICKED");
await sleep_m(10);
collect_point_bonus();
});
}
// ========================
// Twitch Store Observer
// ========================
async function twitch_store_observer() {
// Helper function that detects if the item page is opened
const selector = "#channel-points-reward-center-body > .reward-center-body > div:not(.rewards-list)";
const container = await wait_for_element(selector);
if (container.querySelector(".reward-icon__image")) {
if(GM_config.get("clickable_links_in_description"))
clickable_links_in_description(container);
if (GM_config.get("bulk_purchase_panel"))
insert_twitch_store_amount_panel(container);
}
// Wait till panel disappears
await wait_for_element_to_disappear(selector);
twitch_store_observer();
}
// ========================
// Clickable links in description
// ========================
function clickable_links_in_description(container) {
const desc = container.querySelector("p"); // Get the first <p> element
if (desc?.querySelector("a"))
return; // Skip if there are already <a> elements
const url_regex = /https?:\/\/[^\s]+/g; // Simple URL detection
const original_text = desc.textContent;
if (!url_regex.test(original_text))
return; // Skip if there are no URLs
// Replace URLs with clickable <a> tags
const html_with_links = original_text.replace(url_regex, url => {
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
});
desc.innerHTML = html_with_links;
}
// ========================
// Bulk Twitch Store Purchase UI
// ========================
function insert_twitch_store_amount_panel(container) {
if(!document.querySelector(".k-twitch-store-amount-panel")) {
// console.log("insert_twitch_store_amount_panel: Inserting panel.");
// HTML as a complete string → Amount panel + button for bulk purchase
let html = `
<div class="k-twitch-store-amount-panel">
<button type="button" id="k-twitch-store-amount-decrease">-</button>
<input type="number" id="k-twitch-store-amount-value" value="1" min="1" max="100">
<button type="button" id="k-twitch-store-amount-increase">+</button>
<button type="button" id="k-twitch-store-bulk-purchase" class="k-twitch-store-cart-button" title="Start bulk purchase">🛒</button>
</div>
`;
// Insert HTML after 2nd child
if (container.children.length >= 2)
container.children[1].insertAdjacentHTML("afterend", html);
else
return;
// Event Listeners
const input = document.getElementById("k-twitch-store-amount-value");
const btn_decrease = document.getElementById("k-twitch-store-amount-decrease");
const btn_increase = document.getElementById("k-twitch-store-amount-increase");
const bulk_button = document.getElementById("k-twitch-store-bulk-purchase");
btn_decrease.addEventListener("click", () => on_decrease_click(input));
btn_increase.addEventListener("click", () => on_increase_click(input));
input.addEventListener("input", () => on_input_change(input));
// Bulk Purchase Button → Event
bulk_button.addEventListener("click", async () => {
let amount = parseInt(input.value);
if (isNaN(amount) || amount < 1) amount = 1;
else if (amount > 100) amount = 100;
const product = document.querySelector("#channel-points-reward-center-header > div > p").innerHTML;
bulk_purchase_product(product, amount);
});
}
}
function on_decrease_click(input) {
let value = parse_int_safe(input.value, 1);
value = Math.max(1, value - 1);
input.value = value;
}
function on_increase_click(input) {
let value = parse_int_safe(input.value, 1);
value = Math.min(100, value + 1);
input.value = value;
}
function on_input_change(input) {
let value = parseInt(input.value);
if (isNaN(value) || value < 1)
value = 1;
else if (value > 100)
value = 100;
input.value = value;
}
function parse_int_safe(str, fallback) {
const value = parseInt(str);
return isNaN(value) ? fallback : value;
}
// ========================
// Purchase Functions
// ========================
async function purchase_voucher(trigger) {
let voucher = trigger.target.attributes.voucher.value;
let storebutton = document.querySelector(".community-points-summary button");
storebutton.click();
wait_for_element(".rewards-list").then(async () => { // Wait till rewards list is showing
let rewards = document.querySelector(".rewards-list");
let reward = rewards.querySelector(`img[alt="${voucher}"]`);
if(reward) { // Open the voucher buy menu
reward.click();
wait_for_element(".reward-center-body button.ScCoreButton-sc-ocjdkq-0").then(async () => { // Wait till voucher item is showing
let reward_redeem_button = document.querySelector(".reward-center-body button.ScCoreButton-sc-ocjdkq-0");
if(reward_redeem_button.disabled == false)
reward_redeem_button.click();
else {
storebutton.click();
Swal.fire({
icon: "error",
title: "Error",
text: "Reward not available, maybe you reached maximum amount of claims for this stream or you don't have enough channel points!",
theme: "dark",
backdrop: false
});
}
});
}
else
Swal.fire({
icon: "error",
title: "Error",
text: "Reward not found maybe they are disabled at the moment, if not than please contact script creator via Discord!",
theme: "dark",
backdrop: false
});
});
}
async function bulk_purchase_product(product, amount) {
let storebutton = document.querySelector(".community-points-summary button");
let success_count = 0;
Swal.fire({
title: `Purchasing "${product}"...`,
html: `<progress value="0" max="${amount}"></progress><br>0/${amount}`,
icon: "info",
theme: "dark",
backdrop: false,
showConfirmButton: false,
allowOutsideClick: false,
willOpen: () => {
Swal.showLoading();
}
});
for (let i = 0; i < amount; i++) {
storebutton.click();
try {
await wait_for_element(".rewards-list");
let rewards = document.querySelector(".rewards-list");
let reward = rewards.querySelector(`img[alt="${product}"]`);
if (reward) {
reward.click();
await wait_for_element(".reward-center-body button.ScCoreButton-sc-ocjdkq-0");
let reward_redeem_button = document.querySelector(".reward-center-body button.ScCoreButton-sc-ocjdkq-0");
if (reward_redeem_button.disabled == false) {
reward_redeem_button.click();
success_count++;
} else {
Swal.fire({
icon: "error",
title: "Error",
text: `Reward not available anymore! Process stopped after ${success_count} successful purchases.`,
theme: "dark",
backdrop: false
});
storebutton.click();
return;
}
} else {
Swal.fire({
icon: "error",
title: "Error",
text: `Reward "${product}" not found! Process stopped.`,
theme: "dark",
backdrop: false
});
storebutton.click();
return;
}
} catch (err) {
console.error(err);
Swal.fire({
icon: "error",
title: "Error",
text: `Unexpected error occurred. Process stopped after ${success_count} successful purchases.`,
theme: "dark",
backdrop: false
});
storebutton.click();
return;
}
// Update Progress Bar
Swal.update({
html: `<progress value="${i + 1}" max="${amount}"></progress><br>${i + 1}/${amount}`
});
await wait_for_element_to_disappear(".reward-center-body button.ScCoreButton-sc-ocjdkq-0");
}
Swal.fire({
title: "Done!",
text: `${success_count}/${amount} "${product}" purchased successfully.`,
icon: "success",
theme: "dark",
backdrop: false
});
}
// ========================
// Restart Timer by Zosky
// ========================
async function zoskys_restart_timer(mst) {
let max_stream_time = mst;
let update_interval = 2; // Initial interval: 2 second
let time_element = null;
let timer_element = null;
create_timer_element();
await start_timer();
// Function to calculate the remaining time (only hours and minutes)
function calculate_time_left(seconds, max_stream_time) {
let time_left = max_stream_time - seconds;
let h = Math.floor(time_left / 3600);
let m = Math.floor((time_left % 3600) / 60);
// Format the time (only hours and minutes)
return {
hours: h < 10 ? `0${h}` : h,
minutes: m < 10 ? `0${m}` : m,
};
}
// Function to create the timer element if it doesn't exist
function create_timer_element() {
if (!timer_element) {
wait_for_element("section > div").then(async () => {
const infobox = document.querySelector(".channel-info-content");
const timer_html = `
<div id="time_left" style="width: 100%; text-align: center; font-size: x-large; background: purple;">
Waiting for stream time...
</div>
`;
infobox.insertAdjacentHTML("afterbegin", timer_html);
timer_element = document.getElementById("time_left"); // Update reference
});
}
}
// Function to update the timer in the DOM
async function update_timer() {
try {
// Try to find the .live-time element if not already found
if (!time_element) {
time_element = document.querySelector(".live-time");
if (!time_element) return false; // Element not found
}
// Extract the time from the nested <span> or <p> tag
const time_text = time_element.querySelector("span, p").textContent.trim();
// Extract the time parts (HH:MM:SS)
const time_parts = time_text.split(":").map(Number);
// Handle only HH:MM:SS format
if (time_parts.length === 3 && !time_parts.some(isNaN)) {
const [hours, minutes, seconds] = time_parts;
const total_seconds = hours * 3600 + minutes * 60 + seconds;
// Calculate and update the timer (only hours and minutes)
const { hours: h, minutes: m } = calculate_time_left(total_seconds, max_stream_time);
// Update the timer element
timer_element.innerHTML = `Approx ${h}:${m} till stream restart for vouchers`;
return true; // Element found and updated
} else {
return false; // Invalid format
}
} catch (error) {
console.error("Error updating timer:", error);
return false; // Error occurred
}
}
// Start the timer with dynamic intervals
async function start_timer() {
await wait_for_element(".live-time").then(async () => {
time_element = document.querySelector(".live-time");
update_interval = 50; // Switch to 50-second interval
});
while (true) {
// Try to update the timer
const element_found = await update_timer();
// If update_timer returns false, break the loop
if (!element_found) break;
// Wait for the specified interval
await sleep_s(update_interval);
}
console.log("Timer stopped due to an error or invalid format.");
}
}
// ========================
// CSS Styles
// ========================
function hide_powerups() {
GM_addStyle(`
.rewards-list > div:first-of-type,
.rewards-list [class*="bitsRewardListItem"] {
display: none !important;
}
.rewards-list > div {
padding-top: 0 !important;
}
`);
}
GM_addStyle(`
#configuration {
padding: 20px !important;
max-height: 600px !important;
max-width: 500px !important;
background: inherit !important;
}
#configuration * {
background: inherit;
color: inherit;
}
#configuration .section_header {
margin-bottom: 10px !important;
}
#configuration input {
margin-right: 10px;
}
#configuration input[type="text"] {
display: block;
}
#configuration textarea {
width: 100%;
min-height: 70px;
resize: vertical;
}
#configuration_resetLink {
color: var(--color-text-base) !important;
}
.k-actionbutton,
.k-targetbutton,
#configuration_saveBtn,
#configuration_closeBtn {
padding: 10px;
background-color: var(--color-background-button-primary-default);
color: var(--color-text-button-primary);
display: inline-flex;
position: relative;
align-items: center;
justify-content: center;
vertical-align: middle;
overflow: hidden;
text-decoration: none;
text-decoration-color: currentcolor;
white-space: nowrap;
user-select: none;
font-weight: var(--font-weight-semibold);
font-size: 13px;
height: var(--button-size-default);
border-radius: var(--input-border-radius-default);
}
.k-main-container {
min-width: 300px;
position: relative;
background: inherit;
border-top: 2px solid var(--color-background-button-primary-default);
}
.k-main-container.k-draggable {
border: 2px solid var(--color-background-button-primary-default);
position: absolute;
z-index: 100;
}
.k-store-buttongroups {
padding: 0px 15px 15px;
}
.k-buttongroups {
padding: 25px 15px 15px;
}
.k-buttongroup {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.k-buttongroup-label {
font-size: 13px;
}
.k-labelgroup {
margin-top: 5px;
font-size: 17px;
gap: 25px;
display: flex;
}
.k-hidden {
display: none;
}
#k-streamelements_points {
font-size: 12px;
position: absolute;
left: 5px;
top: 5px;
}
#k-streamelements_points .k-points_added {
color: green;
}
#k-streamelements_points .k-points_subtracted {
color: red;
}
#k-panel-buttons {
position: absolute;
top: 5px;
right: 5px;
user-select: none;
font-size: 16px;
display: grid;
gap: 5px;
grid-auto-flow: column;
}
#k-pin-button,
#k-make-draggable-button,
#k-cart-button,
#k-open-settings {
cursor: pointer;
}
#k-grab-handle {
cursor: grab;
}
.k-grid-1 { display: grid; grid-template-columns: repeat(1, min-content); }
.k-grid-2 { display: grid; grid-template-columns: repeat(2, min-content); }
.k-grid-3 { display: grid; grid-template-columns: repeat(3, min-content); }
.k-grid-4 { display: grid; grid-template-columns: repeat(4, min-content); }
.k-grid-5 { display: grid; grid-template-columns: repeat(5, min-content); }
.k-grid-6 { display: grid; grid-template-columns: repeat(6, min-content); }
.k-grid-7 { display: grid; grid-template-columns: repeat(7, min-content); }
.k-grid-8 { display: grid; grid-template-columns: repeat(8, min-content); }
.k-twitch-store-amount-panel {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
border: 2px solid var(--color-twitch-purple);
border-radius: 8px;
margin-top: 10px;
font-weight: bold;
background: rgba(0,0,0,0.1);
}
.k-twitch-store-amount-panel button {
padding: 6px 10px;
font-size: 16px;
cursor: pointer;
}
.k-twitch-store-amount-panel input[type="number"] {
width: 30px;
text-align: center;
font-size: 16px;
background: none;
border: none;
color: inherit;
-webkit-appearance: none;
-moz-appearance: textfield;
}
.k-twitch-store-amount-panel input[type="number"]:focus-visible {
outline: none;
}
.k-twitch-store-cart-button {
border-left: 2px solid var(--color-twitch-purple);
}
`);