// ==UserScript== // @name Weibo Huati Check-in // @description 超级话题集中签到 // @namespace https://greasyfork.org/users/10290 // @version 0.2.20171026 // @author xyau // @match http*://*.weibo.com/* // @match http*://weibo.com/* // @icon https://n.sinaimg.cn/photo/5b5e52aa/20160628/supertopic_top_area_big_icon_default.png // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @connect m.weibo.cn // @connect login.sina.com.cn // @connect passport.weibo.cn // @connect weibo.com // @downloadURL none // ==/UserScript== if (+$CONFIG.islogin) (() => { /** * see config * @const {object} DEFAULT_CONFIG 默认设置 */ const DEFAULT_CONFIG = Object.freeze({ autoCheckin: true, checkNormal: true, openDetail: true, maxHeight: 360, timeout: 5000, retry: 5, delay: 0, }), /** * @const {object} USER 当前用户 * @const {string} USER.UID 用户ID * @const {string} USER.NICK 用户昵称 */ USER = Object.freeze({ UID: $CONFIG.uid, NICK: $CONFIG.nick, }); /** * @global {object} config 脚本设置 * @global {boolean} config.autoCheckin 自动签到 * @global {boolean} config.checkNormal 普话签到 * @global {boolean} config.openDetail 展开详情 * @global {int} config.maxHeight 详情限高(px) * @global {int} config.timeout 操作超时(ms) * @global {int} config.retry 重试次数 * @global {int} config.delay 操作延时(ms) */ let config = Object.assign(Object.assign({},DEFAULT_CONFIG), JSON.parse(GM_getValue(`config${USER.UID}`, '{}'))), /** @global {object} lastCheckin 上次签到记录 */ lastCheckin = JSON.parse(GM_getValue(`lastCheckin${USER.UID}`, '{}')); /** 清理旧版数据 */ ['autoSignbox', 'todaySigned'].forEach((key) => GM_deleteValue(key)); /** 隐藏游戏按钮,替换为超话签到 */ var logname, checkinBtn = document.createElement("li"); checkinBtn.id = 'checkinBtn'; checkinBtn.innerHTML = `s超话签到`; checkinBtn.addEventListener('contextmenu', () => setupConfig(), true); checkinBtn.addEventListener('click', () => huatiCheckin(false), true); function initCheckinBtn() { checkinBtn.style['pointer-events'] = 'auto'; Array.from(checkinBtn.querySelectorAll('em')).forEach((em) => {em.removeAttribute('style');}); checkinBtn.querySelector('.signBtn').innerText = '超话签到'; checkinBtn.title = '左击开始签到/右击配置脚本'; console.groupEnd(logname); } function alterCheckinBtn(text) { checkinBtn.style['pointer-events'] = 'none'; Array.from(checkinBtn.querySelectorAll('em')).forEach((em) => {em.style.color = '#fa7d3c';}); checkinBtn.querySelector('.signBtn').innerText = `${text}中…`; } let addBtn = setInterval(() => { if (document.querySelector('.gn_nav_list li:last-child')) { clearInterval(addBtn); document.querySelector('.gn_nav_list li:last-child').before(checkinBtn); document.querySelector('a[nm="game"]').parentNode.style.display = 'none'; } /** 自动签到 */ if (config.autoCheckin) huatiCheckin(); }, 100); /** @param {boolean} auto 自动开始*/ function huatiCheckin(auto=true) { console.group(logname='微博超话签到'); /** * 任务构造,初始化通用 xhr 参数 * @constructor * @param {string} name 任务名称 * @param {object} options 附加 xhr 参数 * @param {function} load 成功加载函数 * @param {function} retry 重试函数 * @param {function} [retryButton=] 重试按钮函数 */ var Task = window.Task || function (name, options, load, retry, retryButton) { this.name = name; this.onerror = function(errorType='timeout') { initLog(name, 0); log[name] += 1; if (errorType != 'timeout') console.error(`${name}异常,最终网址为${this.xhr.finalUrl},返回值为${this.xhr.response}`); if (log[name] < config.retry + 1) { setStatus(name + (errorType === 'timeout' ? `超过${config.timeout / 1e3}秒` : '异常') + `,第${log[name]}次重试…`); retry(); } else { setStatus(`${name}超时/异常${log[name]}次,停止自动重试`); if (retryButton) retryButton(); else clearTask(); } }; this.xhrConfig = { synchoronous: false, timeout: config.timeout, onloadstart: () => { currentTask = this; if (!log.hasOwnProperty(name)) setStatus(`${name}…`); if (retryButton) { /** 跳过按钮 */ let skipHuati = document.createElement('a'); skipHuati.classList.add('S_ficon'); skipHuati.style = 'cusor: pointer'; skipHuati.onclick = () => { this.xhr.abort(); retryButton(); skipHuati.remove(); }; skipHuati.innerText = '[跳过]'; checkinStatus.appendChild(skipHuati); } }, onload: (xhr) => { if (xhr.finalUrl.includes('login')) { xhr.timeout = 0; /** 登录跳转 */ let loginJump = GM_xmlhttpRequest({ method: 'GET', synchronous: false, timeout: config.timeout, url: /url='([^']+)'/.exec(xhr.responseText)[1], onloadstart: () => this.xhr = loginJump, onload: (xhr) => this.load(xhr), ontimeout: xhr.ontimeout, }); } else this.load(xhr); }, ontimeout: () => this.onerror(), }; Object.assign(this.xhrConfig, options); this.load = (xhr) => setTimeout(load(xhr), config.delay); this.xhr = GM_xmlhttpRequest(this.xhrConfig); }; /** * 获取话题列表 * @param {object[]} [huatiList=[]] 话题列表 * @param {string} huatiList[].name 名称 * @param {string} huatiList[].hash 编号 * @param {int|null} huatiList[].level 超话等级 * @param {boolean} huatiList[].checked 超话已签 * @param {string} [type='super'] 超话或普话, 'super'/'normal' * @param {int} [total=0] 关注话题数量 * @param {int} [page=1] 列表页码 */ function getHuatiList(huatiList=[], type='super', total=0, page=1) { let getPage = new Task( `获取${type === 'super' ? '超' : '普'}话列表第${page}页`, { method: 'GET', url: `https://m.weibo.cn/api/container/getIndex?containerid=100803_-_page_my_follow_${type}&page=${page}`, }, (xhr) => parsePage(xhr), () => getHuatiList(huatiList, type, total, page) ); function parsePage(xhr) { let data = JSON.parse(xhr.responseText); if (!data.cardlistInfo) { getPage.onerror('error'); } else { if (page === 1) total += data.cardlistInfo.total; data.cards[0].card_group.forEach(function(card) { if (card.card_type === 4) { let name = card.desc.slice(1, -1), level = type === 'super' ? +/level(\d+)\./.exec(card.icon)[1] : null, checked = !!card.avatar_url, hash = null, element = null; if (lastHuatiList && lastHuatiList.includes(name)) { if (lastCheckinDate != date) { let huati = log['待签'].find((huati) => name === huati.name); if (checked) huati = log['待签'].splice(log['待签'].findIndex((huati) => name === huati.name), 1).pop(); hash = huati.hash; element = huati.element; } else { hash = log['已签'][name]; element = document.getElementById(`${hash}`); } } else { hash = /100808([\w\d]+)&/.exec(card.scheme)[1]; element = initElement(name, hash); } huatiList.push({name, checked, hash, level}); if (checked) { if (!lastHuatiList || (lastHuatiList.includes(name) ? lastCheckinDate != date : true)) { checkinDone.appendChild(element); initLog('已签', {}); log['已签'][name] = hash; } } else if (!lastHuatiList || (lastHuatiList.includes(name) ? lastCheckinDate === date && type === "super" : true)){ checkinToDo.appendChild(element); initLog('待签', []); log['待签'].push({name, hash, element}); } if (level) setStatus(`Lv.${level}`, element, true); } }); if (huatiList.length < total) getHuatiList(huatiList, type, total, page + 1); else if (config.checkNormal && type != 'normal') getHuatiList(huatiList, 'normal', total); else { setStatus(`关注列表获取完毕,共${total}个${config.checkNormal ? '话题' : '超话'},` + (log.hasOwnProperty('待签') ? `${log['待签'].length}个待签` : '全部已签')); console.table(huatiList); console.info(log); if (log.hasOwnProperty('待签')) { if (config.autoCheckin) checkin(log['待签'].shift()); else { clearTask(); /** 开始签到按钮 */ let startCheckin = document.createElement('a'); startCheckin.classList.add('S_ficon'); startCheckin.style = 'cusor: pointer'; startCheckin.onclick = () => checkin(log['待签'].shift()); startCheckin.innerText = '[开始签到]'; checkinStatus.appendChild(startCheckin); } } else { clearTask(); initCheckinBtn(); } } } } } /** * 话题签到 * @param {object} huati 话题,参见 {@link getHuatiList#huatiList} * @param {boolean} checkinAll 签到全部话题 */ function checkin(huati, checkinAll=true) { let huatiCheckin = new Task( `${huati.name}话题签到`, { method: 'GET', url: `https://weibo.com/p/aj/general/button?ajwvr=6&api=http://i.huati.weibo.com/aj/super/checkin&texta=签到&textb=已签到&status=0&id=100808${huati.hash}`, }, (xhr) => { let data = JSON.parse(xhr.responseText), code = +data.code; if (code === 100000 || code === 382004) { checkinDone.appendChild(huati.element); log['已签'][huati.name] = huati.hash; Object.assign(lastCheckin, {date, nick: USER.NICK}); Object.assign(lastCheckin, log['已签']); GM_setValue(`lastCheckin${USER.UID}`, JSON.stringify(lastCheckin)); } else { initLog('异常', {}); log['异常'][huati.name] = {huati: huati, code: data.code, msg: data.msg}; huatiCheckin.onerror('error'); } if (code === 100000) setStatus(`签到第${/\d+/g.exec(data.data.alert_title)[0]}名,经验+${/\d+/g.exec(data.data.alert_subtitle)[0]}`, huati.element); else setStatus(data.msg, huati.element); if (checkinAll) { if (log['待签'].length > 0) checkin(log['待签'].shift()); else { clearTask(); setStatus(`${date} 签到完成`); checkinToDo.parentNode.style.display = 'none'; checkinDone.parentNode.setAttribute('open', ''); Object.assign(lastCheckin, {allChecked: true}); GM_setValue(`lastCheckin${USER.UID}`, JSON.stringify(lastCheckin)); console.info(log); initCheckinBtn(); } } }, () => checkin(huati, false), () => { log['待签'].push(huati); if (log['待签'].length > 0) checkin(log['待签'].shift()); else clearTask(); let retryHuati =document.createElement('a'); retryHuati.classList.add('S_ficon'); retryHuati.style = 'cusor: pointer'; retryHuati.onclick = () => checkin(Object.assign({}, huati), false); retryHuati.innerText = '[重试]'; setStatus(retryHuati, huati.element, true); } ); } function clearTask() { currentTask = null; checkinClose.title = '关闭'; } function initLog(key, initialValue) { if (!log.hasOwnProperty(key)) log[key] = initialValue; } function initElement(name, hash) { let element = document.createElement('li'); element.id = hash; element.innerHTML = `.${name}`; return element; } /** * @global {object} log 操作日志 * @global {object[]} log['已签'] 已签话题列表 * @global {object[]} log['待签'] 待签话题列表 * @global {object} log['异常'] 签到异常列表 */ let log = {}, /** @global {string} date 当前东八区日期 */ date = new Date(new Date().getTime() + 288e5).toJSON().substr(0, 10).replace(/-0?/g, '/'), /** @global {Task|null} currentTask 当前 xhr 任务 */ currentTask = null; alterCheckinBtn('签到'); if (!lastCheckin.date || lastCheckin.date != date || !lastCheckin.allChecked || !auto) { /** 设置信息展示界面 */ let checkinCSS = document.getElementById('checkinCSS') || document.createElement('style'); checkinCSS.id = 'checkinCSS'; checkinCSS.type = 'text/css'; checkinCSS.innerHTML = `#checkinInfo {z-index:10000;position:fixed;left: 0px;bottom: 0px;min-width:320px;max-width: 640px;opacity: 0.9}#checkinInfo .W_layer_title {border-top: solid 1px #fa7f40}#checkinStatus {float: right;padding: 0 60px 0 10px}#checkinMore {right: 36px}#checkinClose {right: 12px}#checkinDetail {display: ${config.openDetail ? '' : 'none'};margin: 6px 12px;padding: 2px;max-height: ${config.maxHeight}px;overflow-y:auto;}${scrollbarStyle('#checkinDetail')}#checkinDetail summary {margin: 2px}#checkinDetail ol {column-count: 3}#checkinDetail li {line-height: 1.5}#checkinDetail a {cusor: pointer}#checkinDetail .info {float: right}#checkinStatus ~ .W_ficon {position: absolute;bottom: 0px;font-size: 18px;}`; document.head.appendChild(checkinCSS); let checkinInfo = document.getElementById('checkinInfo') || document.createElement('div'); checkinInfo.id = 'checkinInfo'; checkinInfo.classList.add('W_layer'); checkinInfo.innerHTML = `
${USER.NICK}${config.openDetail ? 'c' : 'd'}X
`; document.body.appendChild(checkinInfo); var checkinStatus = document.getElementById('checkinStatus'), checkinMore = document.getElementById('checkinMore'), checkinClose = document.getElementById('checkinClose'), checkinDetail = document.getElementById('checkinDetail'), checkinDone = document.querySelector('#checkinDone ol'), checkinToDo = document.querySelector('#checkinToDo ol'); checkinMore.onclick = function() { if (this.innerText === 'd') { this.innerText = 'c'; this.title = '收起'; checkinDetail.style.display = ''; } else { this.innerText = 'd'; this.title = '详情'; checkinDetail.style.display = 'none'; } }; checkinClose.onclick = function() { if (currentTask) { currentTask.xhr.abort(); setStatus(`${currentTask.name}中止`); clearTask(); initCheckinBtn(); } else { checkinInfo.remove(); checkinCSS.remove(); initCheckinBtn(); } }; [checkinToDo, checkinDone].forEach((ol, i) => ['DOMNodeInserted', 'DOMNodeRemoved'].forEach((event) => ol.addEventListener(event, function() { let isRemoval = event != 'DOMNodeInserted'; if (this.parentNode.style.display === 'none') this.parentNode.removeAttribute('style'); this.previousSibling.innerText = `${i ? '已' : '待'}签${ol.childElementCount - (isRemoval ? 1 : 0)}个话题`; Array.from(this.querySelectorAll('li .order')).forEach((span) => span.innerText = Array.from(span.parentNode.parentNode.querySelectorAll('li')).findIndex((li) => li === span.parentNode) + (isRemoval ? 0 : 1)); }))); /** 开始获取话题列表 */ checkinClose.title = '中止'; if (lastCheckin.date) { setStatus(`从${lastCheckin.date}签到记录读取话题列表`); var lastHuatiList = [], lastCheckinDate = lastCheckin.date; for (let key in lastCheckin) { if (!['date', 'nick', 'allChecked'].includes(key)) { lastHuatiList.push(key); let hash = lastCheckin[key], element = initElement(key, hash); if (lastCheckinDate != date) { checkinToDo.appendChild(element); initLog('待签', []); log['待签'].push({name:key, hash, element}); } else { checkinDone.appendChild(element); initLog('已签', {}); log['已签'][key] = hash; } } } if (lastCheckinDate != date) lastCheckin = {}; if (log.hasOwnProperty('待签')) { setStatus(`关注列表读取完毕,共${log['待签'].length}个话题待签`); if (config.autoCheckin) checkin(log['待签'].shift()); else { /** 开始签到按钮 */ let startCheckin = document.createElement('a'); startCheckin.classList.add('S_ficon'); startCheckin.style = 'cusor: pointer'; startCheckin.onclick = () => checkin(log['待签'].shift()); startCheckin.innerText = '[开始签到]'; checkinStatus.appendChild(startCheckin); } } } getHuatiList(); } else initCheckinBtn(); } function setupConfig() { console.group(logname='微博超话签到设置'); alterCheckinBtn('设置'); let configCSS = document.createElement('style'); configCSS.id = 'configCSS'; configCSS.type = 'text/css'; configCSS.innerHTML = `#configForm {z-index:6666;position:fixed;right: 0px;top: 50px;width:540px;opacity: 0.9}#configForm form {height: 288px}#configForm header {text-align: center}#configClose {position: absolute;z-index: 2;left: 12px;top: 2px;font-size: 18px;}#configForm header img {position: relative;top: 3px;padding-right: 6px}#configForm footer {position: absolute;bottom: 0px;padding: 12px;width: 492px;border-top: solid 1px #ccc}#configForm footer input {margin: 0 12px}#configForm main {margin: 6px 12px;}#configForm fieldset:first-child {width: 240px;float:left;margin-right: 12px}#configForm fieldset {padding: 1px 12px}#configForm fieldset > fieldset > legend {text-align: right}#configForm input[type=number] {width: 48px}#configForm input[type=button] {padding: 0 12px}#configForm th {font-weight: bold;padding-top: 6px}#configForm table {float: left;margin: 0 6px}#configForm pre {padding: 6px;height: 160px;overflow-y: scroll;background-color: whitesmoke;line-height: 1.5}${scrollbarStyle('#configForm pre')}#configForm span {float: right}`; document.head.appendChild(configCSS); let configForm = document.createElement('div'); configForm.id = 'configForm'; configForm.classList.add('W_layer'); configForm.innerHTML = `
签到脚本设置X
参数设定
签到偏好自动签到
普话签到
视觉偏好自动展开签到详情
详情最大高度 像素
运行参数请求延时 毫秒
请求超时 毫秒
超时及异常自动重试
账户信息
昵称
${USER.NICK}
ID
${USER.UID}
上次签到
${lastCheckin.date || '尚无记录'}
${Object.keys(lastCheckin).filter((key) => !['date', 'nick', 'allChecked'].includes(key)).join('\n')}
`; document.body.appendChild(configForm); let inputs = Array.from(document.querySelectorAll('#configForm fieldset > input')), getInputs = () => inputs.reduce((conf, input) => { conf[input.name] = input.type != 'checkbox' ? +input.value : input.checked; return conf; }, {}), configRestore = document.getElementById('configRestore'), configDefault = document.getElementById('configDefault'); document.getElementById('configSave').onclick = function() { config = getInputs(); GM_setValue(`config${USER.UID}`, JSON.stringify(config)); initForm(); }; configRestore.onclick = () => initForm(); configDefault.onclick = function() { console.warn('重置原设置'); console.table(config); GM_deleteValue(`config${USER.UID}`); initForm(DEFAULT_CONFIG); }; document.getElementById('configClear').onclick = function() { console.warn('清空上次签到'); console.table(lastCheckin); GM_deleteValue(`lastCheckin${USER.UID}`); lastCheckin = {}; document.querySelector('#configForm tr:nth-of-type(6)>td').innerText = '尚无记录'; document.querySelector('#configForm pre').innerText = ''; this.disabled = true; }; document.getElementById('configClose').onclick = function() { configCSS.remove(); configForm.remove(); initCheckinBtn(); }; inputs.forEach(function(input) { input.onchange = () => { configRestore.disabled = isEqual(getInputs(), config) ? true : false; configDefault.disabled = isEqual(getInputs(), DEFAULT_CONFIG) ? true : false; }; }); function initForm(conf=config) { config = conf; for (let key in conf) { if (typeof conf[key] === 'boolean') document.getElementsByName(key)[0].checked = conf[key]; else document.getElementsByName(key)[0].value = conf[key]; } inputs.forEach((input) => input.onchange()); } /** 简单对象比较 * @param {object} x 比较对象x * @param {object} y 比较对象y * @return {boolean} 比较结果 */ function isEqual(x, y) { if (Object.values(x).length != Object.values(y).length) return false; for (let key in x) { if (typeof y[key] === 'undefined' || x[key] != y[key]) return false; } return true; } } /** * 提示签到状态 * @param {string|node} text 当前状态 * @param {node} [element=checkinStatus] 显示提示的节点 * @param {boolean} [append=false] 追加节点 */ function setStatus(text, element=checkinStatus, append=false) { if (element != checkinStatus) element = element.querySelector('.info'); else if (!append) console.info(text); if (append) { if (typeof text != 'string') element.appendChild(text); else element.innerHTML += text; } else element.innerHTML = text; } function scrollbarStyle(elementSelector) { return `${elementSelector}::-webkit-scrollbar {width: 4px;background-color: #f2f2f5;border-radius: 2px;}${elementSelector}::-webkit-scrollbar-thumb {width: 4px;background-color: #808080;border-radius: 2px;}`; } })();