// ==UserScript== // @name Weibo Huati Check-in // @description 超级话题集中签到 // @namespace https://greasyfork.org/users/10290 // @version 0.3.2017111121 // @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== window.addEventListener('load', () => { if (!/\bwvr\b/.test(document.cookie)) console.warn( '微博话题签到:尚未登录\nCookie: {\n', document.cookie.replace(/([^;= ]+) ?= ?([^; ]*)(;)?/g, (_, k, v, sp)=> `${k}:${decodeURIComponent(v)}${sp?'\n':''}`), '\n}' ); else { /** * @const {object} DEFAULT_CONFIG 默认设置 * @const {boolean} DEFAULT_CONFIG.autoCheckin 自动签到 * @const {string} DEFAULT_CONFIG.checkinMode 签到模式 * @const {boolean} DEFAULT_CONFIG.checkNormal 普话签到 * @const {boolean} DEFAULT_CONFIG.autoCheckState 自动查询状态 * @const {boolean} DEFAULT_CONFIG.openDetail 展开详情 * @const {int} DEFAULT_CONFIG.maxHeight 详情限高(px) * @const {int} DEFAULT_CONFIG.timeout 操作超时(ms) * @const {int} DEFAULT_CONFIG.retry 重试次数 * @const {int} DEFAULT_CONFIG.delay 操作延时(ms) */ const DEFAULT_CONFIG = Object.freeze({ autoCheckin: true, checkinMode: 'followListMode', checkNormal: true, autoCheckState: false, 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 {string} 记录名称 */ var logName; /** * @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, /** * 任务构造,初始化通用 xhr 参数 * @constructor * @param {string} name 任务名称 * @param {object} options 附加 xhr 参数 * @param {function} load 成功加载函数 * @param {function} retry 重试函数 * @param {function} [retryButton=] 重试按钮函数 */ 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}异常`); console.info(this); } 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 (checkinStatus){ if (!log.hasOwnProperty(name)) setStatus(`${name}…`); if (retryButton) { /* 跳过按钮 */ let skipHuati = document.createElement('a'); skipHuati.classList.add('S_ficon'); 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); }, clearTask = function() { currentTask = null; checkinClose.title = '关闭'; }, initLog = function(key, initialValue) { if (!log.hasOwnProperty(key)) log[key] = initialValue; }, /** * see DEFAULT_CONFIG * @global {object} config 脚本设置 * @global {object} lastCheckin 上次签到记录 * @global {array} whitelist 话题白名单 */ config = Object.assign(Object.assign({},DEFAULT_CONFIG), JSON.parse(GM_getValue(`config${USER.UID}`, '{}'))), lastCheckin = JSON.parse(GM_getValue(`lastCheckin${USER.UID}`, '{}')), whitelist = JSON.parse(GM_getValue(`whitelist${USER.UID}`, '[]')), initCheckinBtn = function() { checkinBtn.style = 'cursor: pointer'; Array.from(checkinBtn.querySelectorAll('em')).forEach((em) => {em.removeAttribute('style');}); checkinBtn.querySelector('.signBtn').innerText = '超话签到'; checkinBtn.title = '左击开始签到/右击配置脚本'; console.groupEnd(logName); }, /* @param {string} operationName 操作名称 */ alterCheckinBtn = function(operationName) { checkinBtn.style.pointerEvents = 'none'; Array.from(checkinBtn.querySelectorAll('em')).forEach((em) => {em.style.color = '#fa7d3c';}); checkinBtn.querySelector('.signBtn').innerText = `${operationName}中…`; }, /* @param {boolean} auto 自动开始*/ huatiCheckin = function(auto=true) { console.group(logName='微博超话签到'); /** * 获取关注话题列表 * @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] 列表页码 */ let getFollowList = function(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), () => getFollowList(huatiList, type, total, page) ), parsePage = function(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 huati = { 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(huati.name)) { if (!todayChecked) { Object.assign(huati, log.待签.find((huati_) => huati_.name === huati.name)); if (huati.checked) Object.assign(huati, log.待签.splice(log.待签.findIndex((huati_) => huati_.name === huati.name), 1).pop()); } else { huati.hash = log.已签[huati.name]; huati.element = document.getElementById(`_${huati.hash}`); } } else { huati.hash = /100808(\w+)&/.exec(card.scheme)[1]; huati.element = initElement(huati.name, huati.hash); } huatiList.push(huati); if (huati.checked) { if (!lastHuatiList || (lastHuatiList.includes(huati.name) ? !todayChecked : true)) { checkinDone.appendChild(huati.element); initLog('已签', {}); log.已签[huati.name] = huati.hash; } } else if (!lastHuatiList || (lastHuatiList.includes(huati.name) ? todayChecked && type === "super" : true)){ checkinToDo.appendChild(huati.element); initLog('待签', []); log.待签.push(huati); } if (huati.level) setStatus(`Lv.${huati.level}`, huati.element); } }); if (huatiList.length < total) getFollowList(huatiList, type, total, page + 1); else if (config.checkNormal && type != 'normal') getFollowList(huatiList, 'normal', total); else { setStatus(`关注列表获取完毕,共${total}个${config.checkNormal ? '话题' : '超话'},` + (log.hasOwnProperty('待签') ? `${log.待签.length}个待签` : '全部已签')); console.table(huatiList); readyCheckin(); } } }; }, readyCheckin = function(){ 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.onclick = () => checkin(log.待签.shift()); startCheckin.innerText = '[开始签到]'; checkinStatus.appendChild(startCheckin); } } else { clearTask(); initCheckinBtn(); } }, /* 获取话题编号 @param {array} list 话题名称列表 */ getHash = function(list) { let name = list.shift(), huatiGetHash = new Task( `${name}话题信息获取`, { method: 'HEAD', url: `https://m.weibo.cn/api/container/getIndex?type=topic&value=${name}`, }, (xhr) => { if (xhr.status === 200) { let regexp = /fid%3D100808(\w+)/g, hash = regexp.exec(xhr.responseHeaders.match(regexp).pop())[1]; let element = initElement(name, hash); checkinToDo.append(element); initLog('待签', []); log.待签.push({name, hash, element}); if (list.length) getHash(list); else { setStatus(`话题列表获取完毕,共${(log.hasOwnProperty('已签') ? Object.keys(log.已签).length : 0) + (log.hasOwnProperty('待签') ? log.待签.length : 0)}个话题` + (log.hasOwnProperty('待签') ? `${log.待签.length}个待签` : '全部已签')); readyCheckin(); } } }); }, getWhitelist = function() { let toDoList = whitelist.slice(0); if (!whitelist.length) { setStatus('尚未设置签到话题白名单![设置]'); checkinStatus.querySelector('a').onclick = () => { setupConfig(); whitelistMode.click(); editWhitelist.click(); whitelistBox.focus(); }; clearTask(); initCheckinBtn(); } else { if (lastHuatiList) { for (let name of lastHuatiList) { if (!whitelist.includes(name)) { if (!todayChecked) { let index = log.待签.findIndex((huati) => huati.name === name); log.待签[index].element.remove(); log.待签.splice(index, 1); } } else toDoList.splice(toDoList.indexOf(name), 1); } } if (toDoList.length) getHash(toDoList); else { setStatus(`话题列表获取完毕,共${(log.hasOwnProperty('已签') ? Object.keys(log.已签).length : 0) + (log.hasOwnProperty('待签') ? log.待签.length : 0)}个话题` + (log.hasOwnProperty('待签') ? `${log.待签.length}个待签` : '全部已签')); readyCheckin(); } } }, /** * 话题签到 * @param {object} huati 话题,参见 {@link getFollowList#huatiList} * @param {boolean} checkinAll 签到全部话题 */ checkin = function(huati, checkinAll=true) { let huatiCheckin = new Task( `${huati.name}话题签到`, { method: 'GET', url: `/p/aj/general/button?api=http://i.huati.weibo.com/aj/super/checkin&id=100808${huati.hash}`, }, (xhr) => { let data = JSON.parse(xhr.responseText), code = +data.code; switch (code) { case 100000: setStatus(`签到第${/\d+/g.exec(data.data.alert_title)[0]}名,经验+${/\d+/g.exec(data.data.alert_subtitle)[0]}`, huati.element, true); case 382004: { if (code != 100000) setStatus('已签', huati.element, true); checkinDone.appendChild(huati.element); initLog('已签', {}); log.已签[huati.name] = huati.hash; Object.assign(lastCheckin, {date, nick: USER.NICK}); Object.assign(lastCheckin, log.已签); GM_setValue(`lastCheckin${USER.UID}`, JSON.stringify(lastCheckin)); break; } default: { setStatus(data.msg, huati.element, true); initLog('异常', {}); log.异常[huati.name] = {huati, code: data.code, msg: data.msg, xhr: xhr}; huatiCheckin.onerror('error'); } } 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(); if (config.autoCheckState) checkState(); } } }, () => 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.onclick = () => checkin(Object.assign({}, huati), false); retryHuati.innerText = '[重试]'; setStatus(retryHuati, huati.element, true); } ); }, initElement = function(name, hash) { /** * 文本限宽输出 * @param {string} text 输入文本 * @param {int} length 宽度限定 * @return {string} 输出文本 */ let shorten = function(text, length) { let count = 0; for (let index in text) { let increment = /[\x00-\x7f]/.test(text[index]) ? 1 : 2; if (count + increment > length - 2) return `${text.substr(0, index)}…`; count += increment; } return text; }, element = document.createElement('li'); element.id = `_${hash}`; element.innerHTML = `.${shorten(name, 12)}`; return element; }; 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}#checkinInfo a {cursor: 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 = `