// ==UserScript==
// @name Weibo Huati Check-in
// @description 超级话题集中签到
// @namespace https://gf.qytechs.cn/users/10290
// @version 0.2
// @author xyau
// @match http*://www.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
// ==/UserScript==
/**
* see #config
* @const {object} DEFAULT_CONFIG 默认设置
*/
const DEFAULT_CONFIG = {
autoCheckin: true,
checkNormal: true,
openDetail: true,
maxHeight: 360,
timeout: 5000,
retry: 5,
},
/**
* @const {object} USER 当前用户
* @const {string} USER.UID 用户ID
* @const {string} USER.NICK 用户昵称
*/
USER = {
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 重试次数
*/
let config = Object.assign(DEFAULT_CONFIG, JSON.parse(GM_getValue(`config${USER.UID}`, '{}'))),
/** @global {string} date 当前东八区日期 */
date = new Date(new Date().getTime() + 288e5).toJSON().split('T').shift().replace(/-/g, '/'),
/** @global {object} lastCheckin 上次签到记录 */
lastCheckin = JSON.parse(GM_getValue(`lastCheckin${USER.UID}`, '{}'));
/** 清理旧版数据 */
['autoSignbox', 'todaySigned'].forEach((key) => GM_deleteValue(key));
/** 隐藏游戏按钮,替换为超话签到 */
var checkinBtn = document.createElement("li");
checkinBtn.innerHTML = `<a href="javascript: void(0);"><em class="W_ficon ficon_checkin S_ficon">s</em><em class="S_txt1 signBtn">超话签到</em></a>`;
checkinBtn.addEventListener('contextmenu', () => setupConfig());
checkinBtn.addEventListener('click', () => huatiCheckin(false), true);
function initCheckinBtn() {
checkinBtn.style['pointer-events'] = 'auto';
checkinBtn.querySelector('.ficon_checkin').classList.add('S_ficon');
checkinBtn.title = '左键单击开始签到/右键单击配置脚本';
}
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.islogin && config.autoCheckin)
huatiCheckin();
}, 100);
/** @param {boolean} auto 自动开始*/
function huatiCheckin(auto=true) {
/** 设置签到按钮 */
checkinBtn.style['pointer-events'] = 'none';
checkinBtn.title = '签到中……';
/**
* 任务构造,初始化通用 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 (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: function() {
currentTask = xhr;
if (!log.hasOwnProperty(name))
setStatus(`${name}……`);
if (retryButton) {
/** 跳过按钮 */
let skipHuati = document.createElement('a');
skipHuati.classList.add('S_ficon');
skipHuati.style = 'cusor: pointer';
skipHuati.onclick = () => {
xhr.abort();
retryButton();
this.remove();
};
skipHuati.innerText = '[跳过]';
checkinStatus.append(skipHuati);
}
},
onload: function(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: () => currentTask = loginJump,
onload: (xhr) => load(xhr),
ontimeout: xhr.ontimeout,
onabort: xhr.onabort,
});
}
else
load(xhr);
},
ontimeout: () => this.onerror(),
onabort: function() {
setStatus(`${name}中止`);
clearTask();
initCheckinBtn();
},
};
this.xhrConfig = Object.assign(this.xhrConfig, options);
let xhr = GM_xmlhttpRequest(this.xhrConfig);
this.xhr = xhr;
};
/**
* 获取话题列表
* @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) {
checkinClose.title = '中止';
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');
console.log(`${getPage.name}异常,返回值为${data}`);
} 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),
hash = /100808([\w\d]+)&/.exec(card.scheme)[1],
level = type === 'super' ?
+/level(\d+)\./.exec(card.icon)[1] : null,
checked = !!card.avatar_url,
element = document.createElement('li');
element.id = hash;
element.innerHTML = `<span class="order"></span>
<a href="/p/100808${hash}" target="_blank">${name}</a>
<span class="info"></span>`;
huatiList.push({name, checked, hash, level});
if (checked) {
element.querySelector('.info').innerText = `Lv.${level}`;
checkinDone.appendChild(element);
initLog('已签', {});
log['已签'][name] = level;
} else {
if (level)
element.querySelector('.info').innerText = `Lv.${level}`;
checkinToDo.appendChild(element);
initLog('待签', []);
log['待签'].push({name, hash, level, element});
}
}
});
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.info('关注列表获取完毕', huatiList, 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.append(startCheckin);
}
} else
clearTask();
}
}
}
}
/**
* 话题签到
* @param {object} huati 话题,参见 {@link getHuatiList#huatiList}
* @param {boolean} checkinAll 签到全部话题
*/
function checkin(huati, checkinAll=true) {
let huatiCheckin = new Task(
`${huati.name}话题签到`,
{
method: 'GET',
url: `/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.level;
lastCheckin = Object.assign(lastCheckin, {date, nick: USER.NICK});
lastCheckin = 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', '');
initCheckinBtn();
lastCheckin = Object.assign(lastCheckin, {allChecked: true});
GM_setValue(`lastCheckin${USER.UID}`, JSON.stringify(lastCheckin));
console.info('话题签到完毕', log);
}
}
},
() => checkin(huati, false),
() => {
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);
}
);
}
/**
* 提示签到状态
* @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');
if (append)
element.append(text);
else
element.innerHTML = text;
}
function clearTask() {
currentTask = null;
checkinClose.title = '关闭';
}
function initLog(key, initialValue) {
if (!log.hasOwnProperty(key))
log[key] = initialValue;
}
/**
* @global {object} log 操作日志
* @global {object[]} log['已签'] 已签话题列表
* @global {object[]} log['待签'] 待签话题列表
* @global {object} log['异常'] 签到异常列表
*/
let log = {},
/** @global {Task|null} currentTask 当前 xhr 任务 */
currentTask = null;
if (!lastCheckin.date || lastCheckin.date != date)
lastCheckin = {};
if (!lastCheckin.allChecked || !auto) {
/** 设置信息展示界面 */
if (document.querySelector('#checkinCSS'))
document.querySelector('#checkinCSS').remove();
let checkinCSS = document.createElement('style');
checkinCSS.id = 'checkinCSS';
checkinCSS.type = 'text/css';
checkinCSS.innerHTML = `#checkinDetail {
display: ${config.openDetail ? '' : 'none'};
margin: 12px 12px 0 12px;
padding: 2px;
max-height: ${config.maxHeight}px;
overflow-y:auto;
}
#checkinDetail::-webkit-scrollbar {
right: 0px;
width: 4px;
}
#checkinDetail summary {
margin: 5px;
border-bottom: 1px #fa7f40;
}
#checkinDetail ol {column-count: 3}
#checkinDetail li {padding: 2px}
#checkinDetail a {cusor: pointer}
#checkinDetail .info {float: right}
#checkinStatus ~ .W_ficon {
position: absolute;
top: 0px;
font-size: 18px;
}`;
document.head.appendChild(checkinCSS);
if (document.querySelector('#checkinInfo'))
document.querySelector('#checkinInfo').remove();
let checkinInfo = document.createElement('div');
checkinInfo.id = 'checkinInfo';
checkinInfo.classList.add('W_layer');
checkinInfo.style = 'z-index:10000; position:fixed; left: 0px; bottom: 5px; min-width:320px; max-width: 640px; opacity: 0.9';
checkinInfo.innerHTML = `<div class="content" node-type="autoHeight">
<div node-type="inner"><div id="checkinDetail">
<details id="checkinToDo" open style="display: none"><summary class="W_f14 W_fb">待签</summary><ol></ol></details>
<details id="checkinDone" style="display: none"><summary class="W_f14 W_fb">已签</summary><ol></ol></details>
</div></div>
<div node-type="title" class="W_layer_title">${USER.NICK}<span id="checkinStatus" style="float: right; padding: 0 60px 0 10px; border-top: 2px #fa7f40"></span>
<a id="checkinMore" style="right: 36px;" title="${config.openDetail ? '收起' :'详情'}" class="W_ficon S_ficon">${config.openDetail ? 'c' : 'd'}</a>
<a id="checkinClose" style="right: 12px;" node-type="close" title="${currentTask ? '中止' : '关闭'}" class="W_ficon S_ficon">X</a></div>
</div>`;
document.body.appendChild(checkinInfo);
var checkinStatus = document.querySelector('#checkinStatus'),
checkinMore = document.querySelector('#checkinMore'),
checkinClose = document.querySelector('#checkinClose'),
checkinDetail = document.querySelector('#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.abort();
else {
checkinInfo.remove();
checkinCSS.remove();
}
};
[checkinToDo, checkinDone].forEach((ol, i) =>
['DOMNodeInserted', 'DOMNodeRemoved'].forEach((event) =>
ol.addEventListener(event, function() {
if (this.parentNode.style.display === 'none')
this.parentNode.style.display = '';
this.previousSibling.innerText = `${i ? '已' : '待'}签${ol.childElementCount}个话题`;
Array.from(this.querySelectorAll('li .order')).forEach((span) =>
span.innerText = Array.from(span.parentNode.parentNode.querySelectorAll('li')).findIndex((li) =>
li === span.parentNode) + 1);
})));
/** 设置签到按钮 */
checkinBtn.querySelector('.ficon_checkin').classList.remove('S_ficon'); //签到过程中,高亮签到按钮
/** 开始获取话题列表 */
getHuatiList();
} else
initCheckinBtn();
}
function setupConfig() {
let configForm = document.createElement('div');
configForm.id = 'configForm';
configForm.classList.add('W_layer');
configForm.style = 'z-index:10000; position:fixed; right: 0px; top: 50px; width:300px; opacity: 0.9';
configForm.innerHTML = `<form class="content">
<header class="W_layer_title"> <h3>微博超话签到脚本设置(施工中)</h3><a style="right: 12px;" node-type="close" title="关闭" class="W_ficon S_ficon" id="configClose">X</a></header>
<fieldset> <legend> 参数设定 </legend> <fieldset>
<legend>签到偏好</legend>
<input type="checkbox" name="autoCheckin" ${config.autoCheckin ? 'checked' : ''}>自动签到 <br>
<input type="checkbox" name="checkNormal" ${config.checkNormal ? 'checked' : ''}>普话签到 </fieldset>
<fieldset> <legend>视觉偏好</legend>
<input type="checkbox" name="openDetail" ${config.openDetail ? 'checked' : ''}>自动展开签到详情 <br> 详情最大高度(像素):
<input type="number" name="maxHeight" value=${config.maxHeight} min=60 max=1080> </fieldset>
<fieldset> <legend>运行参数</legend> 请求超时(毫秒): <input type="number" name="timeout" value=${config.timeout}>
<br> 超时及异常自动重试次数: <input type="number" name="retry" value=${config.retry}> </fieldset>
<footer> <input type="button" value="保存" name="save" id="configSave"> <input type="button" value="还原" name="restore" id="configRestore"> </footer> </fieldset>
<fieldset> <legend>账户信息 </legend> <span>用户ID:${USER.UID}</span><span>昵称:${USER.NICK}</span> <br>
<fieldset> <legend> 最后签到:${lastCheckin.date} </legend> ${JSON.stringify(lastCheckin)} </fieldset> </fieldset>
<footer>Bug反馈请到
<a href=https://gf.qytechs.cn/zh-CN/scripts/32143-weibo-huati-check-in/feedback target=_blank>GreasyFork</a>
或 <a href=https://gist.github.com/xyauhideto/b9397058ca3166b87e706cbb7249bd54 target=_blank>Gist</a> @xyau</footer> </form>`;
document.body.append(configForm);
document.querySelector('#configSave').onclick = function() {
config = Array.from(document.querySelectorAll('fieldset fieldset input')).reduce((config, input) => {
config[input.name] = input.type != 'checkbox' ? +input.value : input.checked;
return config;
}, {});
GM_setValue(`config${USER.UID}`, JSON.stringify(config));
};
document.querySelector('#configRestore').onclick = function() {
config = DEFAULT_CONFIG;
for (let key in DEFAULT_CONFIG) {
if (typeof DEFAULT_CONFIG[key] === 'boolean')
document.getElementsByName(key)[0].checked = DEFAULT_CONFIG[key];
else
document.getElementsByName(key)[0].value = DEFAULT_CONFIG[key];
}
};
document.querySelector('#configClose').onclick = () => configForm.remove();
}