您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Weekly post count + Merit received + custom goals + local board details + customizable excluded boards
// ==UserScript== // @name Bitcointalk weekly post & Merit Tracker // @namespace https://bitcointalk.org // @version 1.9.9 // @description Weekly post count + Merit received + custom goals + local board details + customizable excluded boards // @author Ace // @match https://bitcointalk.org/* // @grant none // ==/UserScript== (function () { 'use strict'; // Lista CORRETTA degli ID delle SOLE board locali (per paese/lingua) const localBoardIds = [ // Italiano 28, 107, 132, 153, 162, 169, 171, 170, 115, 144, 145, 165, 175, 200, 205, // Portoghese 29, 69, 131, 134, 135, 181, 206, // Spagnolo 27, 101, 102, 103, 104, 105, 130, 151, // Cinese 30, 117, 118, 119, 146, 196, // Russo 10, 18, 20, 21, 22, 23, 55, 66, 72, 90, 91, 185, 236, 237, 248, 256, // Francese 13, 47, 48, 49, 50, 54, 149, 183, 184, 186, 187, 188, 208, 210, 211, 258, // Tedesco 16, 269, 36, 60, 61, 62, 63, 64, 139, 140, 152, 270, // Olandese 79, 80, 94, 116, 143, 147, 148, 150, // Turco 133, 155, 156, 157, 158, 174, 180, 189, 190, 230, 232, 235, 239, 265, // Polacco 142, 163, 164, 263, 264, // Indonesiano 191, 192, 193, 194, 276, 277, 278, // Croato 201, 220, 221, 272, 273, // Filippino 219, 243, 260, 268, 274, // Arabo 241, 242, 253, 266, 267, 271, // Giapponese 252, 255, // Nigeriano 275, 279, 280, // Greco 120, 136, 179, 195, 246, 247, // Ebraico 95, // Rumeno 108, 109, 110, 111, 112, 113, 114, 166, 259 ]; const usernames = ['*ace*', 'lillominato89']; let selectedUser = localStorage.getItem('btwk_user') || usernames[0]; let startDayIndex = parseInt(localStorage.getItem(`btwk_dayIndex_${selectedUser}`)) || 5; // Friday let timezoneOffset = parseInt(localStorage.getItem(`btwk_tzOffset_${selectedUser}`)) || 0; let currentWeekOffset = 0; let collapsed = localStorage.getItem('btwk_collapsed') === 'true'; // Default excluded boards: Offtopic (9), Games & Rounds (71), Mega Threads (243), Services (52) const defaultExcludedBoards = { 9: true, 71: true, 243: true, 52: true }; // Available boards to exclude const availableBoards = [ { id: 9, name: "Offtopic" }, { id: 71, name: "Games & Rounds" }, { id: 243, name: "Mega Threads" }, { id: 52, name: "Services" } ]; const defaultGoals = { minGambling: 10, maxLocal: 5, maxValidPosts: 20, showMerits: true, excludedBoards: { ...defaultExcludedBoards }, excludeLocalBoards: false }; function getUserGoals(user) { const str = localStorage.getItem(`btwk_goals_${user}`); if (!str) return { ...defaultGoals }; try { const goals = JSON.parse(str); if (!goals.excludedBoards) { goals.excludedBoards = { ...defaultExcludedBoards }; } return goals; } catch (e) { console.error("Error parsing goals:", e); return { ...defaultGoals }; } } function saveUserGoals(user, goals) { localStorage.setItem(`btwk_goals_${user}`, JSON.stringify(goals)); } function getWeekRange(offset = 0) { const now = new Date(); const utc = new Date(now.getTime() + timezoneOffset * 60 * 60 * 1000); const day = utc.getUTCDay(); const daysSinceStart = (day + 7 - startDayIndex) % 7; const start = new Date(utc); start.setUTCDate(utc.getUTCDate() - daysSinceStart + offset * 7); start.setUTCHours(0, 0, 0, 0); const end = new Date(start); end.setUTCDate(start.getUTCDate() + 7); end.setUTCHours(0, 0, 0, Math.floor(Math.random() * 1000)); return { from: start.toISOString().split('.')[0], to: end.toISOString(), label: `${start.toISOString().slice(0, 10)} → ${new Date(end - 1).toISOString().slice(0, 10)} (UTC${timezoneOffset >= 0 ? '+' : ''}${timezoneOffset})`, }; } function fetchBoardStats() { const { from, to, label } = getWeekRange(currentWeekOffset); const url = `https://api.ninjastic.space/users/${selectedUser}/boards?from=${from}&to=${to}`; fetch(url) .then((res) => res.json()) .then((json) => { if (!json || json.result !== 'success' || !json.data) { renderStats(`❌ Error fetching data`); return; } const boards = json.data.boards || []; const totalWithBoard = json.data.total_results_with_board || 0; const totalAll = json.data.total_results || 0; const unclassified = totalAll - totalWithBoard; let gambling = 0; let local = 0; let excluded = 0; let localBoardsDetail = {}; const otherBoards = []; const goals = getUserGoals(selectedUser); const excludedBoardIds = Object.keys(goals.excludedBoards) .filter(id => goals.excludedBoards[id]) .map(Number); const excludeLocalBoards = goals.excludeLocalBoards; boards.forEach((b) => { if (!b || !b.key) return; // Gambling boards (ID 228 and 56) if ([228, 56].includes(b.key)) { gambling += b.count; } // Excluded boards (customizable) else if (excludedBoardIds.includes(b.key)) { excluded += b.count; otherBoards.push({ name: `⛔ ${b.name}`, count: b.count }); } // Local boards (IDs in localBoardIds list) else if (localBoardIds.includes(b.key)) { if (!excludeLocalBoards) { local += b.count; if (!localBoardsDetail[b.name]) { localBoardsDetail[b.name] = 0; } localBoardsDetail[b.name] += b.count; } else { excluded += b.count; otherBoards.push({ name: `⛔ ${b.name}`, count: b.count }); } } // Other boards else { otherBoards.push({ name: b.name, count: b.count }); } }); const validLocal = Math.min(local, goals.maxLocal); const excessLocal = local > goals.maxLocal ? local - goals.maxLocal : 0; const validTotal = totalAll - excluded - unclassified - excessLocal; const gamblingCheck = gambling >= goals.minGambling ? '✅' : '❌'; const localCheck = (local === 0) ? '✔️' : (local >= goals.maxLocal ? '✅' : '☑️'); let html = `<b>👤 Account:</b> ${selectedUser} <span id="btwk_settings_btn" style="cursor:pointer;">⚙️</span><br>`; html += `<b>📅 Week:</b><br>${label}<br><br>`; html += `🧮 <b>Post valid:</b> (${validTotal} / ${goals.maxValidPosts}) Total: ${totalAll}<br>`; html += `🧩 <b>Unclassified:</b> ${unclassified}<br>`; html += `🃏 <b>Gambling:</b> ${gambling} / min ${goals.minGambling} ${gamblingCheck}<br>`; html += `🌍 <b>Local:</b> ${local} / max ${goals.maxLocal} ${localCheck}<br>`; if (gamblingCheck === '✅' && localCheck === '✅') { html += `<div style="font-family: monospace; color: #00ff00; margin: 5px 0; font-weight: bold;">WELL DONE!</div>`; } if (Object.keys(localBoardsDetail).length > 0) { html += `<b>📌 Local boards:</b><br>`; for (const [boardName, count] of Object.entries(localBoardsDetail)) { html += `• ${boardName}: ${count}<br>`; } } html += `<br>`; if (otherBoards.length > 0) { html += `<b>📌 Other boards:</b><br>`; otherBoards.forEach((b) => { html += `• ${b.name}: ${b.count}<br>`; }); } renderStats(html); addSettingsListener(); }) .catch((err) => { renderStats(`⚠️ Network error: ${err.message}`); }); } function fetchMerits() { const { from, to } = getWeekRange(currentWeekOffset); const url = `https://api.allorigins.win/get?url=${encodeURIComponent(`https://bpip.org/smerit.aspx?to=${selectedUser}&start=${from}&end=${to}`)}`; fetch(url) .then((res) => res.json()) .then((data) => { if (!data || !data.contents) { renderMerits(`❌ Error loading Merits: No data received`); return; } const html = data.contents; const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const table = doc.querySelector('table'); if (!table) { renderMerits(`❌ Error loading Merits: No table found`); return; } const rows = Array.from(table.querySelectorAll('tbody tr')); const fromMap = {}; let total = 0; rows.forEach((row) => { const tds = row.querySelectorAll('td'); if (tds.length >= 4) { const from = tds[1].innerText.replace('(Summary)', '').trim(); const amount = parseInt(tds[3].innerText.trim()); fromMap[from] = (fromMap[from] || 0) + amount; total += amount; } }); let htmlOut = `<b>⭐ Merits received: ${total}</b><br>`; if (total === 0) { htmlOut += `No Merits received this week.`; } else { Object.entries(fromMap) .sort((a, b) => b[1] - a[1]) .forEach(([from, count]) => { htmlOut += `• ${from}: ${count}<br>`; }); } renderMerits(htmlOut); }) .catch((err) => { renderMerits(`❌ Error loading Merits: ${err.message}`); }); } function renderStats(html) { const div = document.getElementById('btwk_stats'); if (div) div.innerHTML = html; } function renderMerits(html) { const div = document.getElementById('btwk_merits'); if (div) div.innerHTML = html; } function addSettingsListener() { const btn = document.getElementById('btwk_settings_btn'); if (!btn) return; btn.onclick = () => toggleSettingsBox(); } function toggleSettingsBox() { let box = document.getElementById('btwk_settings_box'); if (box) return box.remove(); const goals = getUserGoals(selectedUser); const dayOptions = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] .map((day, i) => `<option value="${i}" ${i === startDayIndex ? 'selected' : ''}>${day}</option>`) .join(''); const excludedBoardsHTML = availableBoards.map(board => { const isChecked = goals.excludedBoards[board.id] ? 'checked' : ''; return ` <label style="display: block; margin: 5px 0;"> <input type="checkbox" id="exclude_${board.id}" ${isChecked}> Exclude ${board.name} </label> `; }).join(''); box = document.createElement('div'); box.id = 'btwk_settings_box'; box.style.marginTop = '8px'; box.style.padding = '8px'; box.style.background = '#333'; box.style.color = '#eee'; box.style.borderRadius = '10px'; box.innerHTML = ` <b>⚙️ Settings for <i>${selectedUser}</i></b><br><br> <label>Minimum Gambling: <input type="number" id="goalMinGambling" min="0" value="${goals.minGambling}" style="width:60px; margin-left:8px;"> </label><br><br> <label>Maximum Local: <input type="number" id="goalMaxLocal" min="0" value="${goals.maxLocal}" style="width:60px; margin-left:18px;"> </label><br><br> <label>Max Valid Posts: <input type="number" id="goalMaxValidPosts" min="0" value="${goals.maxValidPosts}" style="width:60px; margin-left:10px;"> </label><br><br> <label>📅 Week starts on: <select id="btwk_day_select" style="margin-left:4px;">${dayOptions}</select> </label><br><br> <label>🕓 UTC <input type="number" id="btwk_tz_input" value="${timezoneOffset}" style="width:40px; margin-left:18px;"> </label><br><br> <label> <input type="checkbox" id="btwk_show_merits" ${goals.showMerits ? 'checked' : ''}> Show Merits received </label><br><br> <details style="margin-top: 10px; margin-bottom: 10px;"> <summary style="cursor: pointer; font-weight: bold;">📋 Excluded Boards</summary> ${excludedBoardsHTML} <label style="display: block; margin: 5px 0;"> <input type="checkbox" id="excludeLocalBoards" ${goals.excludeLocalBoards ? 'checked' : ''}> Exclude Local Boards </label> </details> <button id="saveGoalsBtn" style="padding:4px 10px; cursor:pointer;">💾 Save</button> `; const container = document.getElementById('btwk_content'); container.appendChild(box); document.getElementById('saveGoalsBtn').onclick = () => { const newMin = parseInt(document.getElementById('goalMinGambling').value) || 0; const newMax = parseInt(document.getElementById('goalMaxLocal').value) || 0; const newMaxValidPosts = parseInt(document.getElementById('goalMaxValidPosts').value) || 0; const newDayIndex = parseInt(document.getElementById('btwk_day_select').value); const newTzOffset = parseInt(document.getElementById('btwk_tz_input').value) || 0; const newShowMerits = document.getElementById('btwk_show_merits').checked; const newExcludeLocalBoards = document.getElementById('excludeLocalBoards').checked; const newExcludedBoards = { ...defaultExcludedBoards }; availableBoards.forEach(board => { newExcludedBoards[board.id] = document.getElementById(`exclude_${board.id}`).checked; }); saveUserGoals(selectedUser, { minGambling: newMin, maxLocal: newMax, maxValidPosts: newMaxValidPosts, showMerits: newShowMerits, excludedBoards: newExcludedBoards, excludeLocalBoards: newExcludeLocalBoards }); startDayIndex = newDayIndex; timezoneOffset = newTzOffset; localStorage.setItem(`btwk_dayIndex_${selectedUser}`, startDayIndex); localStorage.setItem(`btwk_tzOffset_${selectedUser}`, timezoneOffset); box.remove(); updateBoxContent(); update(); }; } function renderBox() { if (document.getElementById('btwk_box')) return; const box = document.createElement('div'); box.id = 'btwk_box'; box.style.position = 'fixed'; box.style.bottom = '10px'; box.style.right = '10px'; box.style.background = '#222'; box.style.color = '#fff'; box.style.padding = '12px'; box.style.borderRadius = '12px'; box.style.fontSize = '13px'; box.style.width = '280px'; box.style.zIndex = '9999'; box.style.boxShadow = '0 0 8px rgba(0,0,0,0.6)'; const toggleBtn = document.createElement('button'); toggleBtn.innerText = collapsed ? '➕' : '➖'; toggleBtn.style.position = 'absolute'; toggleBtn.style.top = '5px'; toggleBtn.style.right = '5px'; toggleBtn.style.background = '#444'; toggleBtn.style.color = '#fff'; toggleBtn.style.border = 'none'; toggleBtn.style.cursor = 'pointer'; toggleBtn.style.fontSize = '14px'; toggleBtn.style.padding = '2px 6px'; toggleBtn.style.borderRadius = '4px'; toggleBtn.onclick = () => { collapsed = !collapsed; localStorage.setItem('btwk_collapsed', collapsed); updateBoxContent(); toggleBtn.innerText = collapsed ? '➕' : '➖'; }; box.appendChild(toggleBtn); const content = document.createElement('div'); content.id = 'btwk_content'; box.appendChild(content); document.body.appendChild(box); updateBoxContent(); } function updateBoxContent() { const container = document.getElementById('btwk_content'); if (!container) return; const goals = getUserGoals(selectedUser); if (collapsed) { container.innerHTML = '<i style="opacity:0.7;">Tracker minimized</i>'; } else { container.innerHTML = ` <div style="margin-bottom:8px;"> <label>👤 <select id="btwk_user_select">${usernames.map((u) => `<option value="${u}"${u === selectedUser ? ' selected' : ''}>${u}</option>`).join('')}</select> </label> </div> <div style="margin-bottom:8px;"> <button id="btwk_prev">⏪</button> <button id="btwk_next">⏩</button> </div> <div id="btwk_stats">⏳ Loading...</div> ${goals.showMerits ? '<hr><div id="btwk_merits">⏳ Loading Merits...</div>' : ''} `; document.getElementById('btwk_user_select').onchange = (e) => { selectedUser = e.target.value; localStorage.setItem('btwk_user', selectedUser); startDayIndex = parseInt(localStorage.getItem(`btwk_dayIndex_${selectedUser}`)) || 5; timezoneOffset = parseInt(localStorage.getItem(`btwk_tzOffset_${selectedUser}`)) || 0; update(); }; document.getElementById('btwk_prev').onclick = () => { currentWeekOffset--; update(); }; document.getElementById('btwk_next').onclick = () => { currentWeekOffset++; update(); }; update(); } } function update() { if (!collapsed) { fetchBoardStats(); const goals = getUserGoals(selectedUser); if (goals.showMerits) fetchMerits(); } } renderBox(); update(); })();