// ==UserScript==
// @name Threads 關注者管理工具
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 獲取所有關注者並批量移除追蹤
// @author You
// @match https://www.threads.net/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// 存儲所有關注者
let allFollowers = [];
// 標記腳本是否已經初始化
let isInitialized = false;
// 創建用戶界面
function createUI() {
// 確保不會重複創建UI
if (isInitialized || document.getElementById('threads-unfollower')) {
return;
}
isInitialized = true;
// 創建主容器
const container = document.createElement('div');
container.id = 'threads-unfollower';
container.style.position = 'fixed';
container.style.top = '20px';
container.style.right = '20px';
container.style.zIndex = '9999';
container.style.backgroundColor = '#222';
container.style.border = '1px solid #444';
container.style.padding = '15px';
container.style.borderRadius = '5px';
container.style.boxShadow = '0 2px 10px rgba(0,0,0,0.5)';
container.style.maxHeight = '80vh';
container.style.overflow = 'auto';
container.style.width = '320px';
container.style.fontFamily = 'Arial, sans-serif';
// 標題
const header = document.createElement('h3');
header.textContent = 'Threads 關注者管理';
header.style.margin = '0 0 15px 0';
header.style.color = '#e0e0e0';
header.style.fontSize = '16px';
header.style.textAlign = 'center';
// 按鈕容器
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'space-between';
buttonContainer.style.marginBottom = '15px';
// 獲取關注者按鈕
const fetchButton = document.createElement('button');
fetchButton.textContent = '獲取所有關注者';
fetchButton.style.padding = '8px 12px';
fetchButton.style.backgroundColor = '#4b4b4b';
fetchButton.style.color = '#e0e0e0';
fetchButton.style.border = '1px solid #666';
fetchButton.style.borderRadius = '4px';
fetchButton.style.cursor = 'pointer';
fetchButton.style.flex = '1';
fetchButton.style.marginRight = '10px';
fetchButton.style.fontSize = '14px';
fetchButton.addEventListener('click', fetchAllFollowers);
// 移除所有追蹤按鈕
const removeAllButton = document.createElement('button');
removeAllButton.textContent = '全部移除追蹤';
removeAllButton.style.padding = '8px 12px';
removeAllButton.style.backgroundColor = '#8B0000';
removeAllButton.style.color = 'white';
removeAllButton.style.border = 'none';
removeAllButton.style.borderRadius = '4px';
removeAllButton.style.cursor = 'pointer';
removeAllButton.style.flex = '1';
removeAllButton.style.fontSize = '14px';
removeAllButton.addEventListener('click', removeAllFollowers);
buttonContainer.appendChild(fetchButton);
buttonContainer.appendChild(removeAllButton);
// 狀態顯示區域
const status = document.createElement('div');
status.id = 'follower-status';
status.textContent = '就緒';
status.style.margin = '10px 0';
status.style.color = '#e0e0e0';
status.style.padding = '5px';
status.style.backgroundColor = '#333';
status.style.borderRadius = '3px';
status.style.fontSize = '14px';
// 計數器
const counter = document.createElement('div');
counter.id = 'follower-counter';
counter.textContent = '關注者: 0';
counter.style.margin = '10px 0';
counter.style.color = '#e0e0e0';
counter.style.fontSize = '14px';
// 進度條
const progressBar = document.createElement('div');
progressBar.id = 'progress-bar-container';
progressBar.style.width = '100%';
progressBar.style.backgroundColor = '#444';
progressBar.style.borderRadius = '4px';
progressBar.style.marginTop = '10px';
progressBar.style.display = 'none';
progressBar.style.overflow = 'hidden';
progressBar.style.height = '15px';
const progressFill = document.createElement('div');
progressFill.id = 'progress-bar-fill';
progressFill.style.height = '100%';
progressFill.style.width = '0%';
progressFill.style.backgroundColor = '#4CAF50';
progressFill.style.borderRadius = '4px';
progressFill.style.transition = 'width 0.3s ease';
progressBar.appendChild(progressFill);
// 關注者列表
const followerList = document.createElement('div');
followerList.id = 'follower-list';
followerList.style.marginTop = '15px';
followerList.style.borderTop = '1px solid #444';
followerList.style.paddingTop = '10px';
// 添加元素到容器
container.appendChild(header);
container.appendChild(buttonContainer);
container.appendChild(status);
container.appendChild(counter);
container.appendChild(progressBar);
container.appendChild(followerList);
// 添加到頁面
document.body.appendChild(container);
}
// 更新狀態消息
function updateStatus(message) {
const status = document.getElementById('follower-status');
if (status) {
status.textContent = message;
}
console.log('[Threads 關注者管理]:', message);
}
// 更新關注者計數
function updateCounter() {
const counter = document.getElementById('follower-counter');
if (counter) {
counter.textContent = `關注者: ${allFollowers.length}`;
}
}
// 設置進度條的進度
function updateProgressBar(percent, show = true) {
const progressBar = document.getElementById('progress-bar-container');
const progressFill = document.getElementById('progress-bar-fill');
if (!progressBar || !progressFill) return;
progressBar.style.display = show ? 'block' : 'none';
progressFill.style.width = `${percent}%`;
}
// 安全地設置元素內容
function safelySetInnerHTML(element, html) {
try {
element.innerHTML = html;
} catch (error) {
console.error('無法設置 innerHTML:', error);
// 使用 textContent 作為備用
element.textContent = '內容載入失敗,請檢查控制台。';
}
}
// 在用戶界面中顯示關注者
function displayFollowers() {
const followerList = document.getElementById('follower-list');
if (!followerList) return;
try {
// 清空列表
while (followerList.firstChild) {
followerList.removeChild(followerList.firstChild);
}
if (allFollowers.length === 0) {
const noFollowers = document.createElement('div');
noFollowers.textContent = '沒有找到關注者。';
noFollowers.style.color = '#e0e0e0';
noFollowers.style.padding = '10px 0';
noFollowers.style.textAlign = 'center';
followerList.appendChild(noFollowers);
return;
}
// 創建關注者列表
allFollowers.forEach((follower, index) => {
const item = document.createElement('div');
item.style.display = 'flex';
item.style.alignItems = 'center';
item.style.padding = '10px 0';
item.style.borderBottom = index < allFollowers.length - 1 ? '1px solid #444' : 'none';
item.dataset.userId = follower.pk;
// 頭像
const img = document.createElement('img');
img.src = follower.profilePicUrl;
img.alt = follower.username;
img.style.width = '40px';
img.style.height = '40px';
img.style.borderRadius = '50%';
img.style.marginRight = '15px';
img.style.objectFit = 'cover';
// 用戶名和全名
const userInfo = document.createElement('div');
userInfo.style.flex = '1';
const username = document.createElement('div');
username.textContent = follower.username;
username.style.fontWeight = 'bold';
username.style.color = '#e0e0e0';
username.style.fontSize = '14px';
const fullName = document.createElement('div');
fullName.textContent = follower.fullName || '';
fullName.style.fontSize = '12px';
fullName.style.color = '#aaa';
fullName.style.marginTop = '3px';
userInfo.appendChild(username);
userInfo.appendChild(fullName);
// 移除追蹤按鈕
const removeBtn = document.createElement('button');
removeBtn.textContent = '移除追蹤';
removeBtn.style.padding = '5px 10px';
removeBtn.style.backgroundColor = '#3a3a3a';
removeBtn.style.color = '#e0e0e0';
removeBtn.style.border = '1px solid #555';
removeBtn.style.borderRadius = '4px';
removeBtn.style.cursor = 'pointer';
removeBtn.style.fontSize = '12px';
removeBtn.style.transition = 'all 0.2s ease';
removeBtn.addEventListener('click', async () => {
removeBtn.disabled = true;
removeBtn.textContent = '處理中...';
removeBtn.style.opacity = '0.7';
try {
await removeFollower(follower.pk);
item.style.opacity = '0.5';
removeBtn.textContent = '已移除';
removeBtn.style.backgroundColor = '#555';
} catch (error) {
removeBtn.textContent = '失敗';
removeBtn.style.backgroundColor = '#8B0000';
console.error('移除追蹤失敗:', error);
setTimeout(() => {
removeBtn.disabled = false;
removeBtn.textContent = '重試';
removeBtn.style.backgroundColor = '#3a3a3a';
removeBtn.style.opacity = '1';
}, 2000);
}
});
item.appendChild(img);
item.appendChild(userInfo);
item.appendChild(removeBtn);
followerList.appendChild(item);
});
} catch (error) {
console.error('顯示關注者列表時出錯:', error);
if (error.message.includes('TrustedHTML')) {
updateStatus(`已成功獲取所有 ${allFollowers.length} 位關注者,但顯示存在安全限制。`);
// 創建一個簡單的信息提示
const infoMessage = document.createElement('div');
infoMessage.textContent = `因瀏覽器安全限制,無法顯示完整列表。已獲取 ${allFollowers.length} 位關注者。請查看控制台獲取完整數據。`;
infoMessage.style.color = '#e0e0e0';
infoMessage.style.padding = '10px';
infoMessage.style.backgroundColor = '#444';
infoMessage.style.borderRadius = '4px';
infoMessage.style.margin = '10px 0';
// 清空列表並添加信息
while (followerList.firstChild) {
followerList.removeChild(followerList.firstChild);
}
followerList.appendChild(infoMessage);
} else {
throw error;
}
}
}
// 獲取所有關注者的主函數
async function fetchAllFollowers() {
allFollowers = [];
updateStatus('正在獲取關注者...');
updateProgressBar(0, true);
// 從 cookies 中獲取 CSRF 令牌和用戶 ID
const csrftoken = getCookie('csrftoken');
const userId = getCookie('ds_user_id');
if (!csrftoken || !userId) {
updateStatus('錯誤:缺少所需的 cookies。請確保已登錄 Threads。');
updateProgressBar(0, false);
return;
}
try {
// 開始獲取關注者,無游標(第一頁)
await fetchFollowersPage(null, csrftoken, userId);
// 用結果更新 UI
updateProgressBar(100);
updateStatus(`完成!找到 ${allFollowers.length} 位關注者。`);
displayFollowers();
// 記錄數據以進行調試
console.log('所有關注者:', allFollowers);
} catch (error) {
console.error('獲取關注者時出錯:', error);
updateStatus(`錯誤: ${error.message}`);
updateProgressBar(0, false);
}
}
// 獲取一頁關注者的函數
async function fetchFollowersPage(cursor, csrftoken, userId) {
// 根據是否有游標確定要使用的查詢
const friendlyName = cursor ?
"BarcelonaFriendshipsFollowersTabRefetchableQuery" :
"BarcelonaFriendshipsFollowersTabQuery";
// 根據查詢確定要使用的 doc_id
const docId = cursor ? "9226067564176291" : "9523819394337000";
// 準備請求的變量
const variables = cursor ?
{
"after": cursor,
"first": 20,
"id": userId,
"__relay_internal__pv__BarcelonaIsLoggedInrelayprovider": true,
"__relay_internal__pv__BarcelonaIsCrawlerrelayprovider": false,
"__relay_internal__pv__BarcelonaHasDisplayNamesrelayprovider": false
} :
{
"first": 20,
"userID": userId,
"__relay_internal__pv__BarcelonaIsLoggedInrelayprovider": true,
"__relay_internal__pv__BarcelonaIsCrawlerrelayprovider": false,
"__relay_internal__pv__BarcelonaHasDisplayNamesrelayprovider": false,
"__relay_internal__pv__BarcelonaShouldShowFediverseListsrelayprovider": true
};
// 創建表單數據
const formData = new URLSearchParams();
formData.append('av', '17841467954992287');
formData.append('__user', userId);
formData.append('__a', '1');
formData.append('__req', cursor ? 'r' : 'o');
formData.append('dpr', '1');
formData.append('fb_api_caller_class', 'RelayModern');
formData.append('fb_api_req_friendly_name', friendlyName);
formData.append('variables', JSON.stringify(variables));
formData.append('server_timestamps', 'true');
formData.append('doc_id', docId);
try {
// 發送 API 請求
const response = await fetch('https://www.threads.net/graphql/query', {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
'x-csrftoken': csrftoken,
'x-fb-friendly-name': friendlyName,
'x-ig-app-id': '238260118697367'
},
body: formData,
credentials: 'include'
});
const data = await response.json();
// 處理API回應結構差異,第一次請求使用user,後續請求使用fetch__XDTUserDict
let followersData;
let pageInfo;
if (data.data.user && data.data.user.followers) {
// 第一次請求的結構
followersData = data.data.user.followers.edges;
pageInfo = data.data.user.followers.page_info;
} else if (data.data.fetch__XDTUserDict && data.data.fetch__XDTUserDict.followers) {
// 後續請求的結構
followersData = data.data.fetch__XDTUserDict.followers.edges;
pageInfo = data.data.fetch__XDTUserDict.followers.page_info;
} else {
console.error('未知的API回應結構:', data);
throw new Error('無效的響應數據');
}
// 處理每個關注者
followersData.forEach(edge => {
if (edge.node) {
allFollowers.push({
id: edge.node.id,
pk: edge.node.pk,
username: edge.node.username,
fullName: edge.node.full_name || '',
profilePicUrl: edge.node.profile_pic_url,
followedBy: edge.node.friendship_status.followed_by,
following: edge.node.friendship_status.following
});
}
});
// 更新 UI
updateCounter();
updateStatus(`目前已獲取 ${allFollowers.length} 位關注者...`);
// 檢查是否有更多頁面要獲取
if (pageInfo.has_next_page && pageInfo.end_cursor) {
// 獲取下一頁
await fetchFollowersPage(pageInfo.end_cursor, csrftoken, userId);
}
} catch (error) {
console.error('獲取關注者頁面時出錯:', error);
throw error;
}
}
// 移除追蹤單個用戶
async function removeFollower(userId) {
const csrftoken = getCookie('csrftoken');
const myUserId = getCookie('ds_user_id');
if (!csrftoken || !myUserId) {
throw new Error('缺少 CSRF 令牌或用戶ID');
}
const formData = new URLSearchParams();
formData.append('av', '17841467954992287');
formData.append('__user', myUserId);
formData.append('__a', '1');
formData.append('__req', '1i');
formData.append('dpr', '1');
formData.append('fb_api_caller_class', 'RelayModern');
formData.append('fb_api_req_friendly_name', 'useBarcelonaRemoveFollowerMutation');
formData.append('variables', JSON.stringify({ "userID": userId }));
formData.append('server_timestamps', 'true');
formData.append('doc_id', '9347461858632460');
const response = await fetch('https://www.threads.net/graphql/query', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': csrftoken,
'X-IG-App-ID': '238260118697367',
'X-FB-Friendly-Name': 'useBarcelonaRemoveFollowerMutation'
},
body: formData,
credentials: 'include'
});
const data = await response.json();
if (!data || !data.data || !data.status || data.status !== 'ok') {
console.error('移除追蹤響應異常:', data);
throw new Error('移除追蹤請求失敗');
}
return data;
}
// 移除所有追蹤
async function removeAllFollowers() {
if (allFollowers.length === 0) {
updateStatus('沒有關注者可以移除追蹤。請先獲取關注者列表。');
return;
}
const confirmRemove = confirm(`確定要移除追蹤所有 ${allFollowers.length} 位用戶嗎?\n此操作無法撤銷。`);
if (!confirmRemove) return;
updateProgressBar(0, true);
updateStatus(`開始批量移除追蹤,共 ${allFollowers.length} 位用戶...`);
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
let successCount = 0;
let failCount = 0;
let continuousFailCount = 0;
for (let i = 0; i < allFollowers.length; i++) {
const follower = allFollowers[i];
const progress = Math.round(((i + 1) / allFollowers.length) * 100);
updateProgressBar(progress);
updateStatus(`正在處理 ${i+1}/${allFollowers.length}:${follower.username}`);
try {
await removeFollower(follower.pk);
successCount++;
continuousFailCount = 0;
// 更新UI
const followerItem = document.querySelector(`[data-user-id="${follower.pk}"]`);
if (followerItem) {
followerItem.style.opacity = '0.5';
const removeBtn = followerItem.querySelector('button');
if (removeBtn) {
removeBtn.textContent = '已移除';
removeBtn.style.backgroundColor = '#555';
removeBtn.disabled = true;
}
}
// 避免請求過於頻繁,添加隨機延遲
await delay(Math.random() * 1000 + 1500);
} catch (error) {
console.error(`移除 ${follower.username} 的追蹤失敗:`, error);
failCount++;
continuousFailCount++;
// 如果連續失敗,暫停一段時間
if (continuousFailCount > 3) {
updateStatus(`連續失敗 ${continuousFailCount} 次,暫停中...`);
await delay(5000); // 暫停5秒
}
// 錯誤太多,詢問是否繼續
if (continuousFailCount > 10) {
const continueProcess = confirm(`已發生多次連續錯誤 (${continuousFailCount}次)。\n是否繼續進行?`);
if (!continueProcess) {
updateStatus(`操作已暫停。成功: ${successCount}, 失敗: ${failCount}`);
return;
}
continuousFailCount = 0; // 重置連續失敗計數
}
}
}
updateProgressBar(100);
updateStatus(`完成! 成功移除追蹤 ${successCount} 位用戶,失敗 ${failCount} 位。`);
}
// 獲取 cookie 值的輔助函數
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
// 初始化腳本
function init() {
// 僅在 Threads 網站上運行
if (window.location.hostname === 'www.threads.net') {
// 確保頁面完全加載後再創建UI
if (document.readyState === 'complete' || document.readyState === 'interactive') {
setTimeout(createUI, 1000);
} else {
window.addEventListener('DOMContentLoaded', () => {
setTimeout(createUI, 1000);
});
}
}
}
// 啟動腳本
init();
})();