您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automatically load and invite all followers of any user in batches of 40
// ==UserScript== // @name Invite Recent Users (Auto-load all followers in batches) // @version 1.5.1 // @description Automatically load and invite all followers of any user in batches of 40 // @match https://scratch.mit.edu/studios/* // @license MIT // @grant none // @namespace https://greasyfork.org/users/1491229 // ==/UserScript== (function () { 'use strict'; const studioId = location.pathname.split('/')[2]; if (!studioId) return; const STORAGE_KEY = `invite_recent_users_invited_${studioId}`; function getInvitedUsers() { const data = localStorage.getItem(STORAGE_KEY); return data ? JSON.parse(data) : []; } function saveInvitedUser(username) { const invited = getInvitedUsers(); if (!invited.includes(username)) { invited.push(username); localStorage.setItem(STORAGE_KEY, JSON.stringify(invited)); } } function getCookie(name) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop().split(';').shift(); return null; } function waitForElement(selector, timeout = 7000) { return new Promise((resolve, reject) => { const el = document.querySelector(selector); if (el) return resolve(el); const observer = new MutationObserver(() => { const elNow = document.querySelector(selector); if (elNow) { observer.disconnect(); resolve(elNow); } }); observer.observe(document.body, { childList: true, subtree: true }); if (timeout) setTimeout(() => { observer.disconnect(); reject(new Error('Timeout waiting for element ' + selector)); }, timeout); }); } waitForElement('#sa-studio-followers-button').then(refButton => { const btn = document.createElement('button'); btn.id = 'scratch-invite-recent-btn'; btn.className = 'button'; btn.textContent = 'Invite Recent'; btn.style.marginLeft = '10px'; btn.onclick = () => { if (document.querySelector('#scratch-invite-modal')) { document.querySelector('#scratch-invite-modal').remove(); } else { showPanel(); } }; refButton.parentNode.insertBefore(btn, refButton.nextSibling); }).catch(console.warn); async function checkUserExists(username) { try { const res = await fetch(`https://api.scratch.mit.edu/users/${encodeURIComponent(username)}`); if (!res.ok) return false; const data = await res.json(); return !!data.username; } catch { return false; } } async function fetchUsers(username, offset) { const url = `https://api.scratch.mit.edu/users/${encodeURIComponent(username)}/followers?limit=40&offset=${offset}`; const res = await fetch(url); if (!res.ok) throw new Error('Fetch failed'); const data = await res.json(); return data.map(u => u.username); } async function inviteUser(username) { try { const csrfToken = getCookie('scratchcsrftoken'); if (!csrfToken) throw new Error('No CSRF token found'); const res = await fetch( `https://scratch.mit.edu/site-api/users/curators-in/${studioId}/invite_curator/?usernames=${encodeURIComponent(username)}`, { method: 'PUT', credentials: 'include', headers: { 'X-CSRFToken': csrfToken, 'Content-Type': 'application/json', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'X-Requested-With': 'XMLHttpRequest' }, body: null } ); const json = await res.json(); if (!res.ok) { throw new Error(json?.detail || 'Unknown error'); } return true; } catch (e) { console.error('Invite error:', e); return false; } } async function showPanel() { const modal = document.createElement('div'); modal.id = 'scratch-invite-modal'; modal.style = ` position: fixed; top: 20px; right: 20px; width: 420px; height: 540px; background: white; border: 2px solid #333; padding: 15px; z-index: 10000; overflow-y: auto; font-family: Arial,sans-serif; display: flex; flex-direction: column; border-radius: 5px; box-shadow: 0 0 15px rgba(0,0,0,0.3); `; modal.innerHTML = ` <button title="Close" style="position:absolute;top:5px;right:5px; font-size: 18px; background:none; border:none; cursor:pointer;">×</button> <h3>Invite Recent Users (Auto-batch)</h3> <label for="username-input" style="margin-top:10px;">Username to fetch followers:</label> <input id="username-input" type="text" value="griffpatch" style="width:100%; padding:6px; margin-bottom:10px;"/> <button id="load-user-btn" style="margin-bottom:10px;">Load Followers</button> <ul id="user-list" style="flex-grow:1; overflow-y:auto; border:1px solid #ccc; padding-left:20px; background:#fafafa; border-radius:3px; min-height: 200px;"></ul> <button id="invite-all-btn" style="margin-top:10px;">Invite All</button> <div id="status" style="margin-top:10px; font-size:0.9rem; color:#333; min-height: 24px;"></div> `; document.body.appendChild(modal); modal.querySelector('button[title="Close"]').onclick = () => modal.remove(); const userInput = modal.querySelector('#username-input'); const loadUserBtn = modal.querySelector('#load-user-btn'); const userListEl = modal.querySelector('#user-list'); const inviteAllBtn = modal.querySelector('#invite-all-btn'); const statusEl = modal.querySelector('#status'); let offset = 0; let currentUsers = []; let currentUsername = userInput.value.trim(); let invitedUsers = getInvitedUsers(); let stopLoading = false; const liMap = new Map(); // username => <li> function updateStatus(msg) { statusEl.textContent = msg; } async function loadAllUsers(reset = false) { if (reset) { offset = 0; currentUsers = []; invitedUsers = getInvitedUsers(); userListEl.innerHTML = ''; stopLoading = false; } loadUserBtn.disabled = true; updateStatus('Loading all followers in batches...'); while (!stopLoading) { try { const batch = await fetchUsers(currentUsername, offset); if (batch.length === 0) break; const filtered = batch.filter(u => !invitedUsers.includes(u)); currentUsers.push(...filtered); for (const user of filtered) { const li = document.createElement('li'); li.textContent = user; li.style.color = '#666'; userListEl.appendChild(li); liMap.set(user, li); } offset += 40; updateStatus(`Loaded ${currentUsers.length} users...`); await new Promise(r => setTimeout(r, 500)); } catch (e) { updateStatus('Failed to fetch more users.'); break; } } updateStatus(`Finished loading ${currentUsers.length} new followers`); loadUserBtn.disabled = false; } async function inviteAll() { if (currentUsers.length === 0) { updateStatus('No users to invite.'); return; } updateStatus(`Loaded ${currentUsers.length} users...`); inviteAllBtn.disabled = true; loadUserBtn.disabled = true; for (let i = 0; i < currentUsers.length; i++) { const user = currentUsers[i]; const li = liMap.get(user); updateStatus(`Inviting ${user} (${i + 1} / ${currentUsers.length})`); const success = await inviteUser(user); if (success) { li.textContent = `✅ ${user}`; li.style.color = 'green'; saveInvitedUser(user); updateStatus(`✅ Invited ${user} (${i + 1} / ${currentUsers.length})`); } else { li.textContent = `❌ ${user}`; li.style.color = 'red'; updateStatus(`❌ Failed to invite ${user} (${i + 1} / ${currentUsers.length})`); } await new Promise(r => setTimeout(r, 500)); } updateStatus('All invitations complete!'); currentUsers = []; inviteAllBtn.disabled = false; loadUserBtn.disabled = false; } loadUserBtn.onclick = async () => { const inputUser = userInput.value.trim(); if (!inputUser) { updateStatus('Please enter a username.'); return; } updateStatus('Checking user...'); loadUserBtn.disabled = true; const exists = await checkUserExists(inputUser); if (!exists) { updateStatus('User not found.'); loadUserBtn.disabled = false; return; } currentUsername = inputUser; await loadAllUsers(true); }; inviteAllBtn.onclick = inviteAll; await loadAllUsers(true); } })();