// ==UserScript== // @name 红客技术联盟每日签到 // @namespace https://bbs.tampermonkey.net.cn/ // @version 3.1.2 // @description 自动签到红客技术联盟(chinahonker.cn)领积分,支持可选自动登录、总积分与连续签到天数 // @author Fiallsnu // @icon https://www.chinahonker.cn/favicon.ico // @crontab 0 8-23 * * * // @connect www.chinahonker.cn // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_cookie // @grant GM_openInTab // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_log // ==/UserScript== /* * 隐私说明 * 1. 默认不保存账号密码;只有你主动开启自动登录并输入后,才会以明文形式 * 保存在本机扩展存储(b2_user / b2_pass),不会写进脚本、不会随脚本上传。 * 2. 所有网络请求只发往 www.chinahonker.cn,不经过任何第三方。 * 3. 只读取站点自身的 b2_token 用于签到;自动登录刷新时才会写回该 cookie。 * 4. 本地只额外保存“今天是否已签到 / 是否已提醒 / 登录失败日期”,不含账号信息。 * * 登录说明 * 站点是 B2 主题 + JWT。b2_token 由 /wp-json/jwt-auth/v1/token 登录后签发, * wp-login.php 只建立 WordPress 登录态,不会签发 b2_token。 */ return (function () { 'use strict'; var SITE = 'https://www.chinahonker.cn'; var API = SITE + '/wp-json/b2/v1/'; var LOGIN_API = SITE + '/wp-json/jwt-auth/v1/token'; var COOKIE_NAME = 'b2_token'; // 提前 1 天刷新登录,避免网页端掉线;改成 0 则只在过期时处理 var REFRESH_BEFORE = 24 * 3600; // 配置了账号密码后是否允许自动登录 var AUTO_LOGIN = true; var finish = function () {}; var loginTried = false; try { registerMenus(); } catch (e) {} return new Promise(function (resolve) { finish = resolve; main(); }); function main() { readToken(function (cookie) { var token = cookie && cookie.value; var signedToday = GM_getValue('lastSuccess') === today(); if (signedToday) repeatNotice(); if (token && !expiring(token)) { if (signedToday) { GM_log('[签到] 今日已签到,登录状态正常'); return finish(); } return sign(token, 0); } // token 缺失或即将过期 if (hasAccount()) { GM_log('[签到] 登录即将过期,自动刷新', 'warn'); return login(function (newToken) { saveToken(newToken, function () { if (GM_getValue('lastSuccess') === today()) return finish(); sign(newToken, 0); }); }); } if (signedToday) { if (token) notifyOnce('登录即将过期', '今天已签到,但登录状态快过期了,建议点此重新登录', true); return finish(); } if (token) return notifyLogin('登录已过期'); notifyLogin('尚未登录'); }); } function hasAccount() { return AUTO_LOGIN && !!GM_getValue('b2_user', '') && !!GM_getValue('b2_pass', ''); } function readToken(cb) { GM_cookie.list({ url: SITE + '/', name: COOKIE_NAME }, function (cookies, err) { if (err) { GM_log('[签到] 读取 cookie 失败:' + err, 'error'); return cb(null); } cb(cookies && cookies.length ? cookies[0] : null); }); } function expiring(token) { var exp = jwtExp(token); if (!exp) return false; return Date.now() / 1000 > exp - REFRESH_BEFORE; } function login(cb) { if (loginTried || GM_getValue('loginFailedDate') === today()) { return failLogin('今天自动登录已失败过'); } loginTried = true; GM_xmlhttpRequest({ method: 'POST', url: LOGIN_API, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: 'username=' + encodeURIComponent(GM_getValue('b2_user', '')) + '&password=' + encodeURIComponent(GM_getValue('b2_pass', '')), responseType: 'json', timeout: 15000, onload: function (r) { var d = body(r); if (r.status === 200 && d) { var token = d.token || (d.data && d.data.token); if (token) { GM_log('[签到] 自动登录成功'); return cb(token); } // 接口只 Set-Cookie、不回 token 时,重新读一次 cookie return readToken(function (cookie) { if (cookie && cookie.value) return cb(cookie.value); failLogin('登录成功但未拿到 token'); }); } failLogin(strip(d && d.message) || ('HTTP ' + r.status)); }, onerror: function (e) { failLogin('网络错误:' + e); }, ontimeout: function () { failLogin('登录请求超时'); } }); } function failLogin(msg) { GM_setValue('loginFailedDate', today()); notify('自动登录失败', msg + '(今天不再自动重试,请手动登录)', true); finish(); } function saveToken(token, cb) { var exp = jwtExp(token) || (Math.floor(Date.now() / 1000) + 7 * 86400); GM_cookie.list({ url: SITE + '/', name: COOKIE_NAME }, function (cookies) { var old = cookies && cookies[0]; var details = { name: COOKIE_NAME, value: token, path: (old && old.path) || '/', secure: true, httpOnly: false, expirationDate: exp }; if (old && !old.hostOnly && old.domain) { details.domain = old.domain; } else { details.url = SITE + '/'; } GM_cookie.set(details, function (cookie, err) { if (err) GM_log('[签到] 写入 b2_token 失败:' + err, 'warn'); else GM_log('[签到] 已刷新 b2_token'); if (cb) cb(); }); }); } function sign(token, n) { GM_xmlhttpRequest({ method: 'POST', url: API + 'userMission', headers: { 'Authorization': 'Bearer ' + token }, responseType: 'json', timeout: 15000, onload: function (r) { var d = body(r); GM_log('[签到] userMission HTTP ' + r.status); if (r.status === 200) { if (d && d.mission && typeof d.mission === 'object') { GM_setValue('lastSuccess', today()); return collectStats(token, d.mission, function (stats) { notify('签到成功', buildMessage(d.mission.credit, stats)); finish(); }); } if (d && d.code === 'user_error') { return notifyLogin('登录状态已失效'); } // 200 但没有 mission:服务端认为今天已经签过(常见于从 3.0 升级后本地没有记录) GM_setValue('lastSuccess', today()); notifyOnce('签到结果', strip(d && d.message) || '今日可能已签到'); return finish(); } if (r.status === 401 || r.status === 403) { return notifyLogin('登录状态已失效'); } retry(token, n); }, onerror: function (e) { GM_log('[签到] 网络错误:' + e, 'warn'); retry(token, n); }, ontimeout: function () { GM_log('[签到] 请求超时', 'warn'); retry(token, n); } }); } // 查询总积分与连续签到天数;失败不影响签到结果 function collectStats(token, mission, cb) { var stats = { total: null, days: null }; if (mission && typeof mission.count === 'number') stats.days = mission.count; api('getUserInfo', token, '', function (d) { if (d && d.user_data && typeof d.user_data.credit !== 'undefined') { stats.total = d.user_data.credit; } if (stats.days !== null) return cb(stats); api('getUserMission', token, 'count=10&paged=1', function (m) { var list = m && m.mission_today_list && m.mission_today_list.data; if (Array.isArray(list)) { for (var i = 0; i < list.length; i++) { var it = list[i] || {}; var name = String(it.name || it.title || ''); if (name.indexOf('签到') >= 0 && typeof it.count === 'number') { stats.days = it.count; break; } } } cb(stats); }); }); } function api(path, token, data, cb) { GM_xmlhttpRequest({ method: 'POST', url: API + path, headers: { 'Authorization': 'Bearer ' + token }, data: data || '', responseType: 'json', timeout: 15000, onload: function (r) { cb(r.status === 200 ? body(r) : null); }, onerror: function () { cb(null); }, ontimeout: function () { cb(null); } }); } function buildMessage(credit, stats) { var parts = []; if (credit !== undefined) parts.push('获得 ' + credit + ' 积分'); if (stats && stats.total !== null) parts.push('总积分 ' + stats.total); if (stats && stats.days !== null) parts.push('连续签到 ' + stats.days + ' 天'); return parts.join(' · ') || '今日签到完成'; } function retry(token, n) { if (n >= 2) { notifyOnce('签到失败', '网络异常,请稍后手动签到'); return finish(); } setTimeout(function () { sign(token, n + 1); }, 3000 + Math.random() * 2000); } // 当天已签到后再次运行:前两次正常提示,连续点就进入“那你点吧”彩蛋 function repeatNotice() { var now = Date.now(); var lastDate = GM_getValue('lastRunDate', ''); var last = Number(GM_getValue('lastRunAt', 0)) || 0; var streak = (lastDate === today() && now - last < 5 * 60 * 1000) ? (Number(GM_getValue('clickStreak', 0)) || 0) + 1 : 1; GM_setValue('lastRunDate', today()); GM_setValue('lastRunAt', now); GM_setValue('clickStreak', streak); if (streak === 1) { if (GM_getValue('repeatNotifiedDate') !== today()) { GM_setValue('repeatNotifiedDate', today()); notify('今日已签到', '今日已签到,请明日再来~'); } return; } if (streak === 2) { notify('今日已签到', '今日已签到,请明日再来~ 再点也不会多给的'); return; } var n = Math.min(streak, 999); notify('那你点吧', '签到 ×' + n + (n >= 999 ? '(别点了,明天再来吧)' : '(行吧,你点吧)')); } function notifyLogin(reason) { notifyOnce('未登录', reason + '。点击打开首页,用右上角登录弹窗登录(wp-login.php 无效)', true); finish(); } function notify(t, d, login) { var o = { title: '红客联盟:' + t, text: d || ' ', timeout: 15000 }; if (login) { o.onclick = function () { GM_openInTab(SITE + '/', { active: true }); }; } GM_notification(o); } function notifyOnce(t, d, login) { var key = login ? 'loginNotifiedDate' : 'notifiedDate'; if (GM_getValue(key) === today()) return; GM_setValue(key, today()); notify(t, d, login); } function registerMenus() { GM_registerMenuCommand('① 设置自动登录账号', function () { try { var u = prompt('红客联盟账号(留空取消)'); if (!u) return; var p = prompt('红客联盟密码'); if (!p) return; GM_setValue('b2_user', u); GM_setValue('b2_pass', p); notify('已保存', '账号密码只保存在本机扩展存储,不会随脚本上传'); } catch (e) { notify('请手动设置', '在脚本猫脚本详情 → 存储 中添加 b2_user、b2_pass'); } }); GM_registerMenuCommand('② 清除自动登录账号', function () { GM_setValue('b2_user', ''); GM_setValue('b2_pass', ''); notify('已清除', '本机保存的账号密码已删除'); }); } function body(r) { if (r && r.response && typeof r.response === 'object') return r.response; try { return JSON.parse((r && r.responseText) || '{}'); } catch (e) { return {}; } } function strip(s) { return s ? String(s).replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim() : ''; } function jwtExp(token) { try { var p = String(token).split('.')[1]; if (!p) return 0; p = p.replace(/-/g, '+').replace(/_/g, '/'); while (p.length % 4) p += '='; var json = decodeURIComponent(atob(p).split('').map(function (c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); return JSON.parse(json).exp || 0; } catch (e) { return 0; } } function today() { var d = new Date(); return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); } function pad(n) { return n < 10 ? '0' + n : '' + n; } })();