// ==UserScript== // @name 百度贴吧一键签到 // @namespace https://blog.qitongtingyu.online/ // @version 1.5 // @description 一键签到所有关注的贴吧,按钮实时显示已签到/未签到数量 // @author 原作:栖桐听雨; // @icon https://tb3.bdstatic.com/public/icon/favicon-v2.ico // @match *://tieba.baidu.com/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect tieba.baidu.com // @connect c.tieba.baidu.com // @require https://unpkg.com/crypto-js@4.1.1/core.js // @require https://unpkg.com/crypto-js@4.1.1/md5.js // @license MIT // @run-at document-end // ==/UserScript== (function () { 'use strict'; const colors = { primary: '#a18276', primaryDark: '#8a6f64', primaryLight: '#b89a8f', accent: '#f4b886', bgPrimary: '#fefdfb', bgHighlight: 'rgba(244, 184, 134, 0.3)', textPrimary: '#5c4a42', textWhite: '#ffffff', border: 'rgba(92, 74, 66, 0.12)', success: '#7a9e7e', error: '#c97b7b', info: '#a18276', shadowPrimary: 'rgba(161, 130, 118, 0.35)', shadowPrimaryHover: 'rgba(161, 130, 118, 0.45)', shadowPrimaryActive: 'rgba(161, 130, 118, 0.3)' }; const styles = ` body > #tiebaAutoSignBtn { all: initial; } body > #tiebaAutoSignBtn { position: fixed; bottom: 5px; right: 5px; z-index: 99999; padding: 1px 1px; background: linear-gradient(135deg, ${colors.primary} 0%, ${colors.primaryDark} 100%); color: ${colors.textWhite}; border: none; border-radius: 5px; cursor: pointer; font-size: 15px; font-weight: 500; letter-spacing: 0.5px; box-shadow: 0 6px 2px ${colors.shadowPrimary}; transition: transform 0.15s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.15s cubic-bezier(0.2, 0.8, 0.2, 1), background 0.2s ease; -webkit-tap-highlight-color: transparent; touch-action: manipulation; outline: none; display: block; width: 120px; text-align: center; line-height: 1.5; } body > #tiebaAutoSignBtn:hover { background: linear-gradient(135deg, ${colors.primaryLight} 0%, ${colors.primary} 100%); box-shadow: 0 8px 28px ${colors.shadowPrimaryHover}; transform: translateY(-2px); } body > #tiebaAutoSignBtn:active { transform: translateY(0) scale(0.98); box-shadow: 0 4px 12px ${colors.shadowPrimaryActive}; } body > #tiebaAutoSignBtn:disabled { background: linear-gradient(135deg, #a89a90 0%, #8a7268 100%); cursor: not-allowed; transform: none; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } body > #tiebaAutoSignBtn .sign-count { font-size: 13px; opacity: 0.9; margin-top: 2px; display: block; } @media (max-width: 480px) { body > #tiebaAutoSignBtn { padding: 12px 20px; font-size: 14px; bottom: 16px; right: 16px; border-radius: 24px; min-width: 140px; } body > #tiebaAutoSignBtn .sign-count { font-size: 12px; } } @media (orientation: landscape) and (max-height: 500px) { body > #tiebaAutoSignBtn { bottom: 12px; padding: 10px 18px; font-size: 13px; min-width: 140px; } } `; const CACHE_KEY = 'tieba_sign_cache_v2'; const FAKE_VERSION = '9.7.8.0'; function makeFakeParams(obj) { return Object.assign({ _client_type: 4, _client_version: FAKE_VERSION, _phone_imei: '0'.repeat(15), model: 'HUAWEI P40', net_type: 1, stErrorNums: 1, stMethod: 1, stMode: 1, stSize: 320, stTime: 117, stTimesNum: 1, timestamp: Date.now() }, obj); } function sign(payload) { const sortKeys = Object.keys(payload).sort(); let str = sortKeys.reduce((acc, key) => acc += `${key}=${payload[key]}`, ''); str += 'tiebaclient!!!'; return CryptoJS.MD5(str).toString(); } function getTodayCache() { const today = new Date(); const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; const cache = JSON.parse(GM_getValue(CACHE_KEY, '{}')); if (cache.date !== dateStr) { GM_setValue(CACHE_KEY, JSON.stringify({ date: dateStr, signed: {} })); return new Map(); } return new Map(Object.entries(cache.signed || {})); } function saveToCache(forumName) { const today = new Date(); const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; const cache = JSON.parse(GM_getValue(CACHE_KEY, '{}')); if (cache.date !== dateStr) { cache.date = dateStr; cache.signed = {}; } cache.signed[forumName] = true; GM_setValue(CACHE_KEY, JSON.stringify(cache)); } function createStyles() { const style = document.createElement('style'); style.textContent = styles; document.head.appendChild(style); } function isMobile() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768; } function showErrorPrompt(message) { const defaultTieba = 'redstone_machinery_communication'; const input = prompt(`${message}\n\n已为你填充默认贴吧:${defaultTieba}\n如需添加其他贴吧,请用英文逗号分隔`, defaultTieba); return input ? input.split(',').map(name => name.trim()).filter(name => name).map(name => ({ forum_name: name, is_sign: 0 })) : []; } async function getFollowedTiebaWithSignStatus() { const forumMap = new Map(); const addForums = (forums) => { forums.forEach(f => { const name = f.forum_name?.trim() || f.name?.trim(); const id = f.forum_id?.toString() || f.id?.toString(); const isSign = f.is_sign === 1 || f.is_sign === '1' || f.is_sign === true ? 1 : 0; if (name) { const key = id || name; const existing = forumMap.get(key); if (existing) { if (isSign) existing.is_sign = 1; } else { forumMap.set(key, { forum_name: name, is_sign: isSign }); } } }); }; const fromNewmoindex = async () => { return new Promise(resolve => { GM_xmlhttpRequest({ method: 'POST', url: 'https://tieba.baidu.com/mo/q/newmoindex', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': document.cookie, 'User-Agent': navigator.userAgent }, timeout: 20000, onload: r => { try { const data = JSON.parse(r.responseText || '{}'); if (data.data?.like_forum && Array.isArray(data.data.like_forum)) { addForums(data.data.like_forum); } } catch { } resolve(); }, onerror: () => resolve(), ontimeout: () => resolve() }); }); }; const fromAppApi = async () => { const appCommonHeader = { 'User-agent': `bdtb for Android ${FAKE_VERSION}`, Accept: '', 'Content-Type': 'application/x-www-form-urlencoded' }; let page = 1; let hasMore = true; while (hasMore && page <= 50) { const baseParams = makeFakeParams({ BDUSS: document.cookie.match(/BDUSS=([^;]+)/)?.[1] || '', page_no: page, page_size: 200 }); const params = { ...baseParams, sign: sign(baseParams) }; const dataStr = Object.keys(params).map(k => `${k}=${encodeURIComponent(params[k])}`).join('&'); const response = await new Promise(resolve => { GM_xmlhttpRequest({ method: 'POST', url: 'https://c.tieba.baidu.com/c/f/forum/like', headers: { ...appCommonHeader, 'Cookie': document.cookie }, data: dataStr, timeout: 20000, onload: r => { try { resolve(JSON.parse(r.responseText || '{}')); } catch { resolve({}); } }, onerror: () => resolve({}), ontimeout: () => resolve({}) }); }); if (response.forum_list) { const forums = []; if (response.forum_list.non_gconforum) { const list = Array.isArray(response.forum_list.non_gconforum) ? response.forum_list.non_gconforum : []; forums.push(...list); } if (response.forum_list.gconforum) { const list = Array.isArray(response.forum_list.gconforum) ? response.forum_list.gconforum : []; forums.push(...list); } if (Array.isArray(response.forum_list)) { forums.push(...response.forum_list); } addForums(forums); } hasMore = response.has_more === '1' || response.has_more === 1; page++; await new Promise(r => setTimeout(r, 200)); } }; await fromNewmoindex(); await fromAppApi(); const tiebaList = [...forumMap.values()]; return tiebaList.length > 0 ? tiebaList : showErrorPrompt('自动提取贴吧列表失败'); } async function getTBS() { return new Promise(resolve => { GM_xmlhttpRequest({ method: 'GET', url: 'https://tieba.baidu.com/dc/common/tbs', headers: { 'Cookie': document.cookie }, timeout: 10000, onload: r => { try { resolve(JSON.parse(r.responseText).tbs || ''); } catch { resolve(''); } }, onerror: () => resolve('') }); }); } async function signSingleTieba(name) { return new Promise(async resolve => { const tbs = await getTBS(); const data = `ie=utf-8&kw=${encodeURIComponent(name)}${tbs ? `&tbs=${tbs}` : ''}`; GM_xmlhttpRequest({ method: 'POST', url: 'https://tieba.baidu.com/sign/add', data: data, headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Referer': `https://tieba.baidu.com/f?kw=${encodeURIComponent(name)}`, 'Cookie': document.cookie }, timeout: 15000, onload: r => { try { const res = JSON.parse(r.responseText); switch (res.no) { case 0: resolve({ status: 'success', msg: `${name}:签到成功\n连续签到:${res.data?.uinfo?.cont_sign_num || 0}天 | 总签到:${res.data?.uinfo?.total_sign_num || 0}次\n本吧排名:${res.data?.finfo?.current_rank_info?.sign_count || 0}` }); break; case 1101: resolve({ status: 'existed', msg: `${name}:今日已签到` }); break; case 100002: resolve({ status: 'fail', msg: `${name}:未登录/登录过期` }); break; case 34004: resolve({ status: 'vcode', msg: `${name}:失败(需要验证码)` }); break; default: { const errorMsg = res.error || '未知错误'; if (errorMsg.includes('vcode')) { resolve({ status: 'vcode', msg: `${name}:失败(need vcode)` }); } else { resolve({ status: 'fail', msg: `${name}:失败(${errorMsg})` }); } } break; } } catch { resolve({ status: 'fail', msg: `${name}:解析失败` }); } }, onerror: () => resolve({ status: 'fail', msg: `${name}:网络错误` }), ontimeout: () => resolve({ status: 'fail', msg: `${name}:请求超时` }) }); }); } let currentStats = { signed: 0, unsigned: 0, total: 0 }; let isSigning = false; let refreshTimer = null; async function updateButtonStats(force = false) { if (isSigning && !force) return; try { const list = await getFollowedTiebaWithSignStatus(); const signed = list.filter(t => t.is_sign === 1).length; const unsigned = list.filter(t => t.is_sign !== 1).length; currentStats = { signed, unsigned, total: list.length }; updateButtonText(); } catch { } } function updateButtonText() { const btn = document.getElementById('tiebaAutoSignBtn'); if (!btn || isSigning) return; btn.innerHTML = `贴吧一键签到已签到 ${currentStats.signed} 未签到 ${currentStats.unsigned}`; } async function autoSign() { isSigning = true; const btn = document.getElementById('tiebaAutoSignBtn'); if (btn) { btn.disabled = true; btn.innerHTML = `贴吧一键签到获取列表中...`; } try { const tiebaList = await getFollowedTiebaWithSignStatus(); if (tiebaList.length === 0) { if (btn) btn.innerHTML = `贴吧一键签到未获取到贴吧`; setTimeout(() => { isSigning = false; updateButtonStats(true); }, 2000); return; } const cache = getTodayCache(); const toSign = []; const skipped = []; const failedList = []; const vcodeList = []; for (const t of tiebaList) { if (cache.has(t.forum_name) || t.is_sign === 1) { skipped.push({ status: 'existed', msg: `${t.forum_name}:今日已签到` }); } else { toSign.push(t); } } let [success, existed, fail, contSign] = [0, skipped.length, 0, 0]; if (toSign.length === 0) { if (btn) btn.innerHTML = `贴吧一键签到全部已签到`; setTimeout(() => { isSigning = false; updateButtonStats(true); }, 2000); return; } const baseDelay = 300; const batchSize = 5; const restInterval = 100; for (let i = 0; i < toSign.length; i++) { if (btn) { btn.innerHTML = `贴吧一键签到签到中 ${i + 1}/${toSign.length}`; } const res = await signSingleTieba(toSign[i].forum_name); if (res.status === 'success') { success++; saveToCache(toSign[i].forum_name); const match = res.msg.match(/连续签到:(\d+)天/); if (match) contSign += parseInt(match[1]); } else if (res.status === 'existed') { existed++; saveToCache(toSign[i].forum_name); } else if (res.status === 'vcode') { fail++; vcodeList.push(toSign[i]); } else { fail++; failedList.push(toSign[i]); } if (i !== toSign.length - 1) { const randomOffset = Math.floor(Math.random() * 200) - 100; await new Promise(r => setTimeout(r, Math.max(0, baseDelay + randomOffset))); } if ((i + 1) % batchSize === 0 && i !== toSign.length - 1) { const batchRandom = Math.floor(Math.random() * 1000); await new Promise(r => setTimeout(r, 1500 + batchRandom)); } if ((i + 1) % restInterval === 0 && i !== toSign.length - 1) { const restRandom = Math.floor(Math.random() * 2000); if (btn) { btn.innerHTML = `贴吧一键签到休息中 ${i + 1}/${toSign.length}`; } await new Promise(r => setTimeout(r, 4500 + restRandom)); } } if (vcodeList.length > 0) { const maxVcodeRetries = 3; let currentVcodeList = [...vcodeList]; for (let retryRound = 0; retryRound < maxVcodeRetries && currentVcodeList.length > 0; retryRound++) { if (btn) { btn.innerHTML = `贴吧一键签到验证码重试 ${retryRound + 1}/3`; } await new Promise(r => setTimeout(r, 10000)); const vcodeBaseDelay = 2000; const retryResults = []; for (let j = 0; j < currentVcodeList.length; j++) { const name = currentVcodeList[j].forum_name; const res = await signSingleTieba(name); if (res.status === 'success') { success++; fail--; saveToCache(name); const match = res.msg.match(/连续签到:(\d+)天/); if (match) contSign += parseInt(match[1]); } else if (res.status === 'existed') { existed++; fail--; saveToCache(name); } else { retryResults.push(currentVcodeList[j]); } if (j !== currentVcodeList.length - 1) { const randomOffset = Math.floor(Math.random() * 1000); await new Promise(r => setTimeout(r, vcodeBaseDelay + randomOffset)); } } currentVcodeList = retryResults; } } if (failedList.length > 0) { const maxFailRetries = 3; let currentFailedList = [...failedList]; for (let retryRound = 0; retryRound < maxFailRetries && currentFailedList.length > 0; retryRound++) { if (btn) { btn.innerHTML = `贴吧一键签到失败重试 ${retryRound + 1}/3`; } await new Promise(r => setTimeout(r, 5000)); const retryResults = []; for (let k = 0; k < currentFailedList.length; k++) { const name = currentFailedList[k].forum_name; const res = await signSingleTieba(name); if (res.status === 'success') { success++; fail--; saveToCache(name); const match = res.msg.match(/连续签到:(\d+)天/); if (match) contSign += parseInt(match[1]); } else if (res.status === 'existed') { existed++; fail--; saveToCache(name); } else { retryResults.push(currentFailedList[k]); } if (k !== currentFailedList.length - 1) { await new Promise(r => setTimeout(r, 2000 + Math.floor(Math.random() * 1000))); } } currentFailedList = retryResults; } } if (btn) { btn.innerHTML = `贴吧一键签到完成 成功${success} 失败${fail}`; } } catch { if (btn) { btn.innerHTML = `贴吧一键签到签到出错`; } } setTimeout(() => { isSigning = false; updateButtonStats(true); }, 3000); } function createSignButton() { if (document.getElementById('tiebaAutoSignBtn')) return; const btn = document.createElement('button'); btn.id = 'tiebaAutoSignBtn'; btn.innerHTML = `贴吧一键签到加载中...`; btn.addEventListener('click', async () => { if (btn.disabled) return; await autoSign(); }); document.body.appendChild(btn); } function init() { createStyles(); setTimeout(() => { createSignButton(); updateButtonStats(); if (refreshTimer) clearInterval(refreshTimer); refreshTimer = setInterval(() => updateButtonStats(), 60000); }, isMobile() ? 3000 : 2000); } document.readyState === 'complete' ? init() : window.addEventListener('load', init); })();