// ==UserScript==
// @name Facebook 優化版自動戳回
// @namespace https://github.com/poterpan/tampermonkey-scripts/facebook-autopoke
// @version 3.5.2
// @description 自動在Facebook上戳回朋友,帶有控制面板和統計功能
// @author PoterPan
// @match https://www.facebook.com/pokes
// @match https://www.facebook.com/pokes/*
// @homepageURL https://github.com/poterpan/tampermonkey-scripts
// @supportURL https://github.com/poterpan/tampermonkey-scripts/issues
// ==/UserScript==
(function() {
'use strict';
// 全局設置和計數變數
const settings = {
enabled: true, // 是否啟用自動戳回
minDelay: 3, // 最小延遲(秒)
maxDelay: 30, // 最大延遲(秒)
idleMinDelay: 30, // 閒置狀態最小延遲(秒)
idleMaxDelay: 90, // 閒置狀態最大延遲(秒)
idleThreshold: 10 // 多少次無活動後進入閒置狀態
};
// 統計數據
const stats = {
totalPokes: 0, // 總戳回次數
personalStats: {}, // 每個人的戳回次數
lastPokeTime: null, // 上次戳回時間
nextPokeTime: null, // 下次預計戳回時間
noActivityCount: 0, // 無活動計數
isIdle: false // 是否處於閒置狀態
};
// 追蹤已處理的按鈕
const processedButtons = new Set();
let controlPanel = null; // 控制面板引用
let timerInterval = null; // 倒計時計時器
// 儲存設定到 localStorage
function saveSettings() {
localStorage.setItem('fb_autopoke_settings', JSON.stringify(settings));
console.log('設定已儲存', settings);
}
// 儲存統計到 localStorage
function saveStats() {
localStorage.setItem('fb_autopoke_stats', JSON.stringify(stats));
}
// 創建控制面板
function createControlPanel() {
if (document.getElementById("fb_autopoke_panel")) return;
// 主控制面板
const panel = document.createElement("div");
panel.id = "fb_autopoke_panel";
panel.style.position = "fixed";
panel.style.zIndex = "10000";
panel.style.right = "20px";
panel.style.top = "60px";
panel.style.width = "280px";
panel.style.backgroundColor = "#fff";
panel.style.border = "1px solid #dddfe2";
panel.style.borderRadius = "8px";
panel.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.1)";
panel.style.fontFamily = "Arial, sans-serif";
panel.style.fontSize = "13px";
panel.style.color = "#1c1e21";
// 面板標題
const header = document.createElement("div");
header.style.padding = "10px";
header.style.borderBottom = "1px solid #dddfe2";
header.style.fontWeight = "bold";
header.style.backgroundColor = "#4267b2";
header.style.color = "#fff";
header.style.borderRadius = "8px 8px 0 0";
header.style.display = "flex";
header.style.justifyContent = "space-between";
header.style.alignItems = "center";
header.innerHTML = "Facebook 自動戳回控制面板";
// 最小化按鈕
const minButton = document.createElement("button");
minButton.textContent = "−";
minButton.style.background = "none";
minButton.style.border = "none";
minButton.style.color = "#fff";
minButton.style.fontWeight = "bold";
minButton.style.fontSize = "16px";
minButton.style.cursor = "pointer";
minButton.title = "最小化";
minButton.onclick = function() {
const content = document.getElementById("fb_autopoke_content");
if (content.style.display === "none") {
content.style.display = "block";
this.textContent = "−";
this.title = "最小化";
} else {
content.style.display = "none";
this.textContent = "+";
this.title = "展開";
}
};
header.appendChild(minButton);
// 內容區域
const content = document.createElement("div");
content.id = "fb_autopoke_content";
content.style.padding = "10px";
// 操作區域
const controls = document.createElement("div");
controls.style.marginBottom = "10px";
// 開關按鈕
const toggleBtn = document.createElement("button");
toggleBtn.id = "fb_autopoke_toggle";
toggleBtn.textContent = settings.enabled ? "已啟用" : "已停用";
toggleBtn.style.padding = "5px 10px";
toggleBtn.style.marginRight = "10px";
toggleBtn.style.border = "none";
toggleBtn.style.borderRadius = "4px";
toggleBtn.style.backgroundColor = settings.enabled ? "#42b72a" : "#f5f6f7";
toggleBtn.style.color = settings.enabled ? "#fff" : "#4b4f56";
toggleBtn.style.cursor = "pointer";
toggleBtn.onclick = function() {
settings.enabled = !settings.enabled;
this.textContent = settings.enabled ? "已啟用" : "已停用";
this.style.backgroundColor = settings.enabled ? "#42b72a" : "#f5f6f7";
this.style.color = settings.enabled ? "#fff" : "#4b4f56";
if (settings.enabled) {
scheduleNextPoke(3); // 啟用後3秒開始
}
saveSettings();
};
controls.appendChild(toggleBtn);
// 立即運行按鈕
const runNowBtn = document.createElement("button");
runNowBtn.textContent = "立即戳回";
runNowBtn.style.padding = "5px 10px";
runNowBtn.style.border = "none";
runNowBtn.style.borderRadius = "4px";
runNowBtn.style.backgroundColor = "#42b72a";
runNowBtn.style.color = "#fff";
runNowBtn.style.cursor = "pointer";
runNowBtn.onclick = function() {
if (!settings.enabled) return;
clearTimeout(window.pokeTimeout);
runAutoPoke();
};
controls.appendChild(runNowBtn);
// 狀態訊息
const statusDiv = document.createElement("div");
statusDiv.id = "fb_autopoke_status";
statusDiv.style.marginTop = "10px";
statusDiv.style.marginBottom = "10px";
statusDiv.style.padding = "8px";
statusDiv.style.backgroundColor = "#f5f6f7";
statusDiv.style.borderRadius = "4px";
statusDiv.style.fontSize = "12px";
// 設置區域
const settingsDiv = document.createElement("div");
settingsDiv.style.marginTop = "10px";
settingsDiv.style.marginBottom = "10px";
// 函數來創建設置項
function createSettingItem(label, id, value, min, max) {
const item = document.createElement("div");
item.style.display = "flex";
item.style.justifyContent = "space-between";
item.style.alignItems = "center";
item.style.marginBottom = "5px";
const labelEl = document.createElement("label");
labelEl.htmlFor = id;
labelEl.textContent = label;
const input = document.createElement("input");
input.id = id;
input.type = "number";
input.value = value;
input.min = min;
input.max = max;
input.style.width = "60px";
input.style.padding = "3px";
input.onchange = function() {
const val = parseInt(this.value);
if (isNaN(val) || val < min) this.value = min;
if (val > max) this.value = max;
// 更新設置
const settingKey = id.replace('fb_autopoke_', '');
settings[settingKey] = parseInt(this.value);
saveSettings();
// 記錄變更
addLog(`已設定 ${label} 為 ${this.value}`);
};
item.appendChild(labelEl);
item.appendChild(input);
return item;
}
// 添加各種設置項
settingsDiv.appendChild(createSettingItem("正常最小延遲 (秒):", "fb_autopoke_minDelay", settings.minDelay, 1, 60));
settingsDiv.appendChild(createSettingItem("正常最大延遲 (秒):", "fb_autopoke_maxDelay", settings.maxDelay, 5, 300));
settingsDiv.appendChild(createSettingItem("閒置最小延遲 (秒):", "fb_autopoke_idleMinDelay", settings.idleMinDelay, 10, 300));
settingsDiv.appendChild(createSettingItem("閒置最大延遲 (秒):", "fb_autopoke_idleMaxDelay", settings.idleMaxDelay, 30, 600));
settingsDiv.appendChild(createSettingItem("閒置閾值 (無活動次數):", "fb_autopoke_idleThreshold", settings.idleThreshold, 3, 50));
// 日誌區域
const logDiv = document.createElement("div");
logDiv.id = "fb_autopoke_log";
logDiv.style.marginTop = "10px";
logDiv.style.padding = "8px";
logDiv.style.backgroundColor = "#f8f8f8";
logDiv.style.borderRadius = "4px";
logDiv.style.fontSize = "12px";
logDiv.style.maxHeight = "100px";
logDiv.style.overflowY = "auto";
logDiv.innerHTML = "<div>自動戳回日誌將顯示在這裡</div>";
// 統計區域
const statsDiv = document.createElement("div");
statsDiv.id = "fb_autopoke_stats";
statsDiv.style.marginTop = "10px";
statsDiv.style.fontSize = "12px";
statsDiv.innerHTML = "<div style='font-weight:bold;margin-bottom:5px;'>個人戳回統計:</div>";
// 個人統計列表
const statsList = document.createElement("div");
statsList.id = "fb_autopoke_stats_list";
statsList.style.maxHeight = "150px";
statsList.style.overflowY = "auto";
statsList.style.padding = "5px";
statsList.style.backgroundColor = "#f5f6f7";
statsList.style.borderRadius = "4px";
statsDiv.appendChild(statsList);
// 清除統計按鈕
const clearStatsBtn = document.createElement("button");
clearStatsBtn.textContent = "清除統計";
clearStatsBtn.style.padding = "3px 8px";
clearStatsBtn.style.marginTop = "5px";
clearStatsBtn.style.border = "1px solid #dddfe2";
clearStatsBtn.style.borderRadius = "4px";
clearStatsBtn.style.backgroundColor = "#f5f6f7";
clearStatsBtn.style.cursor = "pointer";
clearStatsBtn.onclick = function() {
stats.totalPokes = 0;
stats.personalStats = {};
updateStatsList();
saveStats();
addLog("已清除所有統計資料");
};
statsDiv.appendChild(clearStatsBtn);
// 組裝面板
content.appendChild(controls);
content.appendChild(statusDiv);
content.appendChild(settingsDiv);
content.appendChild(logDiv);
content.appendChild(statsDiv);
panel.appendChild(header);
panel.appendChild(content);
document.body.appendChild(panel);
// 保存面板引用
controlPanel = panel;
// 更新面板上各項設定的值
updateSettingsDisplay();
// 更新顯示
updateStatus();
updateStatsList();
// 開始計時器
setInterval(updateStatus, 1000);
return panel;
}
// 更新設定顯示
function updateSettingsDisplay() {
document.getElementById("fb_autopoke_minDelay").value = settings.minDelay;
document.getElementById("fb_autopoke_maxDelay").value = settings.maxDelay;
document.getElementById("fb_autopoke_idleMinDelay").value = settings.idleMinDelay;
document.getElementById("fb_autopoke_idleMaxDelay").value = settings.idleMaxDelay;
document.getElementById("fb_autopoke_idleThreshold").value = settings.idleThreshold;
// 更新啟用/停用按鈕狀態
const toggleBtn = document.getElementById("fb_autopoke_toggle");
if (toggleBtn) {
toggleBtn.textContent = settings.enabled ? "已啟用" : "已停用";
toggleBtn.style.backgroundColor = settings.enabled ? "#42b72a" : "#f5f6f7";
toggleBtn.style.color = settings.enabled ? "#fff" : "#4b4f56";
}
}
// 添加日誌
function addLog(message) {
const logDiv = document.getElementById("fb_autopoke_log");
if (!logDiv) return;
const now = new Date();
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const seconds = now.getSeconds().toString().padStart(2, '0');
const timestamp = `${hours}:${minutes}:${seconds}`;
const logEntry = document.createElement("div");
logEntry.innerHTML = `<span style="color:#777;">[${timestamp}]</span> ${message}`;
logDiv.appendChild(logEntry);
// 滾動到底部
logDiv.scrollTop = logDiv.scrollHeight;
// 限制日誌數量
while (logDiv.childNodes.length > 50) {
logDiv.removeChild(logDiv.firstChild);
}
}
// 更新狀態顯示
function updateStatus() {
const statusDiv = document.getElementById("fb_autopoke_status");
if (!statusDiv) return;
const now = new Date();
let status = "";
status += `<div>總計戳回: <b>${stats.totalPokes}</b> 次</div>`;
if (stats.lastPokeTime) {
const lastTime = new Date(stats.lastPokeTime);
const timeDiff = Math.round((now - lastTime) / 1000);
const lastTimeStr = formatTime(lastTime);
status += `<div>上次戳回: <b>${lastTimeStr}</b> (${timeDiff}秒前)</div>`;
} else {
status += `<div>上次戳回: <b>無</b></div>`;
}
if (stats.nextPokeTime && settings.enabled) {
const nextTime = new Date(stats.nextPokeTime);
let timeLeft = Math.round((nextTime - now) / 1000);
if (timeLeft < 0) timeLeft = 0;
const nextTimeStr = formatTime(nextTime);
status += `<div>下次檢查: <b>${nextTimeStr}</b> (<span id="fb_countdown">${timeLeft}</span>秒後)</div>`;
status += `<div>狀態: <b>${stats.isIdle ? "閒置模式" : "活躍模式"}</b> (連續${stats.noActivityCount}次無活動)</div>`;
} else {
status += `<div>下次檢查: <b>已停用</b></div>`;
}
statusDiv.innerHTML = status;
// 更新倒計時
const countdownEl = document.getElementById("fb_countdown");
if (countdownEl && stats.nextPokeTime) {
let timeLeft = Math.round((new Date(stats.nextPokeTime) - now) / 1000);
if (timeLeft < 0) timeLeft = 0;
countdownEl.textContent = timeLeft;
}
}
// 更新個人統計列表
function updateStatsList() {
const statsList = document.getElementById("fb_autopoke_stats_list");
if (!statsList) return;
// 將個人統計轉為陣列並按次數排序
const sortedStats = Object.entries(stats.personalStats)
.sort((a, b) => b[1] - a[1]);
if (sortedStats.length === 0) {
statsList.innerHTML = "<div style='color:#777;font-style:italic;'>尚無數據</div>";
return;
}
let html = "";
sortedStats.forEach(([name, count]) => {
html += `<div style='display:flex;justify-content:space-between;margin-bottom:3px;'>
<span>${name}</span>
<span><b>${count}</b> 次</span>
</div>`;
});
statsList.innerHTML = html;
}
// 格式化時間為 HH:MM:SS
function formatTime(date) {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
}
// 嘗試從元素附近找到用戶名稱
function findUserName(element) {
// 最基本的方法,查找附近的鏈接
let container = element;
let maxDepth = 5;
let depth = 0;
// 向上查找容器
while (container && depth < maxDepth) {
// 查找附近的用戶鏈接
const userLinks = container.querySelectorAll('a[href*="facebook.com/"]');
for (const link of userLinks) {
const text = link.textContent.trim();
if (text && text !== "Facebook" && text !== "戳回去" && !text.includes("http")) {
return text;
}
}
// 從父元素的文本中尋找匹配「某人連續戳你 X 次」的模式
const contentText = container.textContent || "";
const pokeMatch = contentText.match(/([^\s,.!?]+)連續戳你\s*\d+\s*次/);
if (pokeMatch && pokeMatch[1]) {
return pokeMatch[1];
}
// 從父元素的文本中尋找匹配「某人戳了你」的模式
const pokeMatch2 = contentText.match(/([^\s,.!?]+)戳了你/);
if (pokeMatch2 && pokeMatch2[1]) {
return pokeMatch2[1];
}
container = container.parentElement;
depth++;
}
return "未知用戶";
}
// 安排下次戳回檢查
function scheduleNextPoke(seconds) {
if (!settings.enabled) return;
clearTimeout(window.pokeTimeout);
if (!seconds) {
// 根據當前狀態決定延遲時間
let minDelay, maxDelay;
if (stats.isIdle) {
minDelay = settings.idleMinDelay;
maxDelay = settings.idleMaxDelay;
} else {
minDelay = settings.minDelay;
maxDelay = settings.maxDelay;
}
seconds = minDelay + Math.round(Math.random() * (maxDelay - minDelay));
}
addLog(`將在 ${seconds} 秒後進行下次戳回檢查`);
// 更新下次戳回時間
stats.nextPokeTime = new Date(Date.now() + seconds * 1000).getTime();
saveStats();
// 更新顯示
updateStatus();
// 設置定時器
window.pokeTimeout = setTimeout(runAutoPoke, seconds * 1000);
}
// 主要戳回功能
function runAutoPoke() {
if (!settings.enabled) return;
addLog("執行自動戳回檢查...");
console.log("執行自動戳回檢查...");
// 確保控制面板存在
if (!controlPanel) {
createControlPanel();
}
// 找到所有具有戳回按鈕特徵的元素
let pokeButtons = [];
// 方法1: 使用aria-label找戳回按鈕
const ariaButtons = document.querySelectorAll('[aria-label="戳回去"]');
pokeButtons = [...ariaButtons];
// 方法2: 查找包含 "戳回去" 文本的按鈕元素
if (pokeButtons.length === 0) {
const allElements = document.querySelectorAll('div[role="button"]');
for (const el of allElements) {
if (el.textContent && el.textContent.includes("戳回去")) {
pokeButtons.push(el);
}
}
}
// 記錄找到的元素數量
addLog(`找到 ${pokeButtons.length} 個戳回按鈕`);
// 已處理的用戶集,避免一次處理同一用戶多次
let processedUsers = new Set();
// 計數
let newPokes = 0;
let pokedUsers = [];
// 處理每個戳回按鈕
for (const button of pokeButtons) {
// 嘗試找到用戶名稱
const userName = findUserName(button);
// 如果這個用戶已經處理過,跳過
if (processedUsers.has(userName)) {
continue;
}
try {
// 標記為已處理
processedUsers.add(userName);
// 更新統計
stats.totalPokes++;
if (!stats.personalStats[userName]) {
stats.personalStats[userName] = 0;
}
stats.personalStats[userName]++;
// 點擊按鈕
button.click();
newPokes++;
pokedUsers.push(userName);
addLog(`已點擊「${userName}」的戳回按鈕`);
console.log(`已點擊「${userName}」的戳回按鈕`);
} catch (e) {
addLog(`點擊出錯: ${e.message}`);
console.error("點擊出錯:", e);
}
}
// 更新統計信息
if (newPokes > 0) {
stats.lastPokeTime = Date.now();
stats.noActivityCount = 0;
stats.isIdle = false;
addLog(`成功戳回 ${newPokes} 人: ${pokedUsers.join(', ')}`);
} else {
stats.noActivityCount++;
if (stats.noActivityCount >= settings.idleThreshold) {
stats.isIdle = true;
}
addLog(`沒有找到可戳回的用戶,無活動次數: ${stats.noActivityCount}`);
}
// 保存統計
saveStats();
// 更新顯示
updateStatus();
updateStatsList();
// 安排下次檢查
scheduleNextPoke();
}
// 初始化
function initialize() {
console.log("初始化 Facebook 自動戳回腳本...");
// 載入保存的設置
const savedSettings = localStorage.getItem('fb_autopoke_settings');
if (savedSettings) {
try {
const parsed = JSON.parse(savedSettings);
Object.assign(settings, parsed);
console.log("已載入儲存的設定:", settings);
} catch (e) {
console.error("載入設置失敗:", e);
}
}
// 載入保存的統計
const savedStats = localStorage.getItem('fb_autopoke_stats');
if (savedStats) {
try {
const parsed = JSON.parse(savedStats);
Object.assign(stats, parsed);
console.log("已載入儲存的統計:", stats);
} catch (e) {
console.error("載入統計失敗:", e);
}
}
// 創建控制面板
createControlPanel();
// 加入初次載入的日誌
addLog("腳本已初始化" + (settings.enabled ? ",自動戳回已啟用" : ",自動戳回目前停用"));
// 如果設置為啟用,則開始自動戳回
if (settings.enabled) {
// 初始延遲3秒,避免腳本重載時立即運行
scheduleNextPoke(3);
}
}
// 在頁面載入完成後初始化
if (document.readyState === 'complete') {
initialize();
} else {
window.addEventListener('load', initialize);
}
})();