您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Auto-Claims drops, while attempting to evade bot detection and claim quickly.
// ==UserScript== // @name Twitch Drop Auto-Claim // @namespace https://greasyfork.org/en/users/1077259-synthetic // @version 0.13 // @description Auto-Claims drops, while attempting to evade bot detection and claim quickly. // @author @Synthetic // @license MIT // @match https://www.twitch.tv/inventory // @match https://www.twitch.tv/drops/inventory // @icon https://www.google.com/s2/favicons?sz=64&domain=twitch.tv // @grant GM_getValue // @grant GM_setValue // ==/UserScript== (function() { 'use strict'; // The current version const VERSION = 0.13; // Page element selectors const PROGRESS_BAR = 'div.tw-progress-bar'; const CLAIM_DROP = 'button.ScCoreButton-sc-ocjdkq-0.ScCoreButtonPrimary-sc-ocjdkq-1.caieTg.eHSNkH'; // Handy constants const NOW = (new Date()).getTime(); /** * The rate to use if none can be calculated, in seconds. * * The rate is used to calculate the next refresh, * and is defined as the time taken to progress 1%. */ const THIRTY_RATE = 18; // seconds /** * The maximum time before the next refresh, in seconds. * * We don't want to calculate too early, as the longer the timeframe * we have the more accurate the rate calculation will be. * We also don't want to trigger the bot by refreshing too much. */ const MAX_REFRESH = 1800; // seconds /** * The maximum age of a previous read, in seconds. * * If we find a previous read but it's too old we just ignore it, * as it is unlikely to be relevant to this drop. */ const THRESHOLD = 15000; // seconds /** * A buffer to add to the final refresh to ensure we have hit 100%, in seconds. * * As we try to accurately calculate the time required to reach 100% we may fall just short. * This buffer is used to try to ensure we are just over rather than just under. */ const TIME_BUFFER = 10; // seconds /** * A buffer to add when checking expected refresh times. * * Even though we set a refresh of a specific interval the difference * between load times will not exactly match that figure, so we use this buffer * when checking whether the load time is expected. */ const REFRESH_BUFFER = 30; // seconds /** * The delay between clicking multiple Claim Now buttons. * * If we click too quickly we are identified as a bot. */ const CLICK_DELAY = 8000; // miliseconds /** * The delay to wait to see if all mutations are done. * */ const MUTATE_DELAY = 5000; // miliseconds /** * Dumps an object to the console. * * @param object o The object to dump. * @return void */ const dump = (o) => { for (var p in o) { if ((o[p] != null) && (typeof o[p] == 'object')) { console.group(p); dump(o[p]); console.groupEnd(); } else { console.log(p, o[p]); } } } /** * Returns the base storage object. * * @return object */ const getDefaults = () => { return JSON.parse( JSON.stringify( { base: { time: null, progress: null, offset: null, }, last: { time: null, progress: null, expected: null, }, version: VERSION } ) ); } /** * Retrieves stored data * * @return object|boolean */ const getPrevious = () => { var previous = GM_getValue('previous'); if (typeof previous == 'undefined' || previous == false) { return false; } try { previous = JSON.parse(previous); } catch (e) { return false; } return previous; }; /** * Converts a calculated progress rate into a fixed value (30m, 45m, 1hr, 2hr, 3hr, ... 15hr). * * @param integer rate The calculated rate. * @return integer The fixed rate. */ const fixedRate = (rate) => { var diff = 10000; var fixed = THIRTY_RATE; const options = [18, 27]; for (var i = 1; i <= 15; i++) { options.push(i * 36); } options.forEach((r) => { if (Math.abs(rate - r) < diff) { diff = Math.abs(rate - r); fixed = r; } }); return fixed; }; /** * Sets the timer to refresh the page. * * @param integer refresh The number of seconds to wait before refreshing. * @return void */ const setTimer = (refresh) => { console.log('Setting refresh of', refresh, 'seconds'); console.log('Next load', new Date((new Date()).getTime() + refresh * 1000)); window.setTimeout( () => { window.location.reload(); }, refresh * 1000 ); startCountdown(); }; /** * Clicks any Claim button, with a short delay between each click. * * @return Promise */ const claimDrop = new Promise((resolve, reject) => { const nodes = document.querySelectorAll(CLAIM_DROP); console.log(nodes); if (nodes.length == 0) { resolve(false); } for (var i = 0; i < nodes.length; i++) { console.log(nodes[i]); window.setTimeout( (node) => { node.click(); console.log('click') }, i * CLICK_DELAY, nodes[i] ); } window.setTimeout(() => { resolve(true); }, --i * CLICK_DELAY); }); const startCountdown = () => { window.setInterval( () => { document.title = title + ' (' + (--refresh).toString() + ')'; }, 1000 ); }; /** * Runs once the widgets have finally load. * Contains all the logic used to calculate the page refresh. * * @param integer progress The largest progress value. * @return void */ const processPage = () => { var rate = THIRTY_RATE; var progress = 0; var nodes = document.querySelectorAll(PROGRESS_BAR); if (nodes.length) { progresses = [...nodes] .map((node) => { return Number(node.getAttribute('aria-valuenow')); }) .filter((progress) => { return progress <= 100; }) .sort((a, b) => { return a == b ? 0 : (a < b ? -1 : 1) }); progress = progresses.pop(); console.log('Progress', progress); } claimDrop .then((claimed) => { if (claimed) { progress = progresses.pop(); if (typeof progress == 'undefined') { progress = 0; } refresh = rate * (100 - progress); previous = getDefaults(); previous.base = { time: NOW, progress: progress, offset: 0, }; } else { if (previous) { const increase = { base: progress - previous.base.progress, last: progress - previous.last.progress, } if (increase.last < 1) { previous = false; console.log('No increase since last load, resetting data') } else { rate = fixedRate(Math.ceil(interval.base / increase.base)); if (previous.last.expected) { var reduce = true; var diff = 0; console.log('Expected increase of', previous.last.expected); console.log('Actual increase is', increase.last) if (previous.last.expected == increase.last) { diff = Math.floor(previous.last.rate * previous.base.offset); } else if (Math.abs(interval.last - previous.last.refresh) > REFRESH_BUFFER) { console.log('Not a full refesh'); const expected = Math.floor(interval.last / rate); console.log('New expected increase of', expected); if (increase.last > expected) { diff = interval.last - expected * rate; } else { reduce = increase.last < expected; } } if (diff > 0) { console.log('Reduced base time by', diff, 'seconds'); previous.base.time -= diff * 1000; } if (reduce) { previous.base.offset /= 2; if (previous.base.offset < 0.01) { previous.base.offset = 0; } } } } } if (!previous) { rate = THIRTY_RATE; previous = getDefaults(); previous.base = { time: NOW, progress: progress, offset: 0.5, }; } console.log('Rate', rate); refresh = (100 - progress) * rate; previous.last.expected = null; if (previous.last.progress !== null) { if (refresh < MAX_REFRESH) { var p = Math.min(100, previous.base.progress + (interval.base / rate)); console.log('Accurate progress', p.toFixed(3)); // NOTE: // Sometimes p > progress // Do we rely on time/rate (p), and assume ui has not been updated recently, or: // p = Math.min(p, progress + 0.5); refresh = Math.max(rate, Math.ceil((100 - p) * rate) + TIME_BUFFER); } else if (previous.base.offset > 0) { previous.last.expected = Math.floor(MAX_REFRESH / rate); refresh = (previous.last.expected * rate) - Math.floor(rate * previous.base.offset); } } refresh = Math.min(refresh, MAX_REFRESH); console.log('Refresh', refresh); } previous.last.time = NOW; previous.last.progress = progress; previous.last.rate = rate; previous.last.refresh = refresh; GM_setValue('previous', JSON.stringify(previous)); setTimer(refresh); }); } /** * Runs when the dom updates, used to gain access to the progress bars, when they finally load. * * @param array mutationsList The list of mutations. * @return void */ const onMutate = (mutationsList) => { clearTimeout(timeout); clearTimeout(loading); loading = window.setTimeout(processPage, MUTATE_DELAY); }; console.log('Loaded at', new Date()); var timeout; var refresh = null; var interval; var loading; var progresses; const title = document.title; var previous = getPrevious(); if (previous) { console.log('Baseline', new Date(previous.base.time)); console.log('Last seen', new Date(previous.last.time)); interval = { base: Math.round((NOW - previous.base.time) / 1000), last: Math.round((NOW - previous.last.time) / 1000) } console.log('Interval', interval.base); if ((interval.base > THRESHOLD) || (interval.last > MAX_REFRESH + REFRESH_BUFFER)) { previous = false; console.log('Interval is too large'); } } if (!previous) { console.log('No relevant read'); } // If we can't find a progress bar after 10s this will set a refesh timeout = window.setTimeout( () => { GM_setValue('previous', false); refresh = 100 * THIRTY_RATE; setTimer(refresh); }, 10000 ); var observer = new MutationObserver(onMutate); observer.observe(document.body, { childList: true, subtree: true }); })();