Weibo Huati Check-in

超级话题集中签到

当前为 2017-11-12 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Weibo Huati Check-in
  3. // @description 超级话题集中签到
  4. // @namespace https://greasyfork.org/users/10290
  5. // @version 0.3.2017111121
  6. // @author xyau
  7. // @match http*://*.weibo.com/*
  8. // @match http*://weibo.com/*
  9. // @icon https://n.sinaimg.cn/photo/5b5e52aa/20160628/supertopic_top_area_big_icon_default.png
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_deleteValue
  13. // @grant GM_xmlhttpRequest
  14. // @connect m.weibo.cn
  15. // @connect login.sina.com.cn
  16. // @connect passport.weibo.cn
  17. // @connect weibo.com
  18. // ==/UserScript==
  19.  
  20. window.addEventListener('load', () => {
  21. if (!/\bwvr\b/.test(document.cookie))
  22. console.warn(
  23. '微博话题签到:尚未登录\nCookie: {\n',
  24. document.cookie.replace(/([^;= ]+) ?= ?([^; ]*)(;)?/g, (_, k, v, sp)=> `${k}:${decodeURIComponent(v)}${sp?'\n':''}`),
  25. '\n}'
  26. );
  27. else {
  28. /**
  29. * @const {object} DEFAULT_CONFIG 默认设置
  30. * @const {boolean} DEFAULT_CONFIG.autoCheckin 自动签到
  31. * @const {string} DEFAULT_CONFIG.checkinMode 签到模式
  32. * @const {boolean} DEFAULT_CONFIG.checkNormal 普话签到
  33. * @const {boolean} DEFAULT_CONFIG.autoCheckState 自动查询状态
  34. * @const {boolean} DEFAULT_CONFIG.openDetail 展开详情
  35. * @const {int} DEFAULT_CONFIG.maxHeight 详情限高(px)
  36. * @const {int} DEFAULT_CONFIG.timeout 操作超时(ms)
  37. * @const {int} DEFAULT_CONFIG.retry 重试次数
  38. * @const {int} DEFAULT_CONFIG.delay 操作延时(ms)
  39. */
  40. const DEFAULT_CONFIG = Object.freeze({
  41. autoCheckin: true,
  42. checkinMode: 'followListMode',
  43. checkNormal: true,
  44. autoCheckState: false,
  45. openDetail: true,
  46. maxHeight: 360,
  47. timeout: 5000,
  48. retry: 5,
  49. delay: 0,
  50. }),
  51.  
  52. /**
  53. * @const {object} USER 当前用户
  54. * @const {string} USER.UID 用户ID
  55. * @const {string} USER.NICK 用户昵称
  56. */
  57. USER = Object.freeze({
  58. UID: $CONFIG.uid,
  59. NICK: $CONFIG.nick,
  60. });
  61. /* @global {string} 记录名称 */
  62. var logName;
  63.  
  64. /**
  65. * @global {object} log 签到记录
  66. * @global {object[]} log.已签 已签话题列表
  67. * @global {object[]} log.待签 待签话题列表
  68. * @global {object} log.异常 签到异常列表
  69. */
  70. let log = {},
  71.  
  72. /* @global {string} date 当前东八区日期 */
  73. date = new Date(new Date().getTime() + 288e5).toJSON().substr(0, 10).replace(/-0?/g, '/'),
  74.  
  75. /* @global {Task|null} currentTask 当前 xhr 任务 */
  76. currentTask = null,
  77.  
  78. /**
  79. * 任务构造,初始化通用 xhr 参数
  80. * @constructor
  81. * @param {string} name 任务名称
  82. * @param {object} options 附加 xhr 参数
  83. * @param {function} load 成功加载函数
  84. * @param {function} retry 重试函数
  85. * @param {function} [retryButton=] 重试按钮函数
  86. */
  87. Task = window.Task || function (name, options, load, retry, retryButton) {
  88. this.name = name;
  89. this.onerror = function(errorType='timeout') {
  90. initLog(name, 0);
  91. log[name] += 1;
  92.  
  93. if (errorType != 'timeout') {
  94. console.error(`${name}异常`);
  95. console.info(this);
  96. }
  97.  
  98. if (log[name] < config.retry + 1) {
  99. setStatus(name + (errorType === 'timeout' ? `超过${config.timeout / 1e3}秒` : '异常') + `,第${log[name]}次重试…`);
  100. retry();
  101. } else {
  102. setStatus(`${name}超时/异常${log[name]}次,停止自动重试`);
  103.  
  104. if (retryButton)
  105. retryButton();
  106. else
  107. clearTask();
  108. }
  109. };
  110. this.xhrConfig = {
  111. synchoronous: false,
  112. timeout: config.timeout,
  113. onloadstart: () => {
  114. currentTask = this;
  115. if (checkinStatus){
  116. if (!log.hasOwnProperty(name))
  117. setStatus(`${name}…`);
  118. if (retryButton) {
  119. /* 跳过按钮 */
  120. let skipHuati = document.createElement('a');
  121. skipHuati.classList.add('S_ficon');
  122. skipHuati.onclick = () => {
  123. this.xhr.abort();
  124. retryButton();
  125. skipHuati.remove();
  126. };
  127. skipHuati.innerText = '[跳过]';
  128. checkinStatus.appendChild(skipHuati);
  129. }
  130. }
  131. },
  132. onload: (xhr) => {
  133. if (xhr.finalUrl.includes('login')) {
  134. xhr.timeout = 0;
  135. /* 登录跳转 */
  136. let loginJump = GM_xmlhttpRequest({
  137. method: 'GET',
  138. synchronous: false,
  139. timeout: config.timeout,
  140. url: /url=&#39;([^']+)&#39;/.exec(xhr.responseText)[1],
  141. onloadstart: () => this.xhr = loginJump,
  142. onload: (xhr) => this.load(xhr),
  143. ontimeout: xhr.ontimeout,
  144. });
  145. }
  146. else
  147. this.load(xhr);
  148. },
  149. ontimeout: () => this.onerror(),
  150. };
  151.  
  152. Object.assign(this.xhrConfig, options);
  153.  
  154. this.load = (xhr) => setTimeout(load(xhr), config.delay);
  155. this.xhr = GM_xmlhttpRequest(this.xhrConfig);
  156. },
  157.  
  158. clearTask = function() {
  159. currentTask = null;
  160. checkinClose.title = '关闭';
  161. },
  162.  
  163. initLog = function(key, initialValue) {
  164. if (!log.hasOwnProperty(key))
  165. log[key] = initialValue;
  166. },
  167.  
  168. /**
  169. * see DEFAULT_CONFIG
  170. * @global {object} config 脚本设置
  171. * @global {object} lastCheckin 上次签到记录
  172. * @global {array} whitelist 话题白名单
  173. */
  174. config = Object.assign(Object.assign({},DEFAULT_CONFIG), JSON.parse(GM_getValue(`config${USER.UID}`, '{}'))),
  175. lastCheckin = JSON.parse(GM_getValue(`lastCheckin${USER.UID}`, '{}')),
  176. whitelist = JSON.parse(GM_getValue(`whitelist${USER.UID}`, '[]')),
  177.  
  178. initCheckinBtn = function() {
  179. checkinBtn.style = 'cursor: pointer';
  180. Array.from(checkinBtn.querySelectorAll('em')).forEach((em) => {em.removeAttribute('style');});
  181. checkinBtn.querySelector('.signBtn').innerText = '超话签到';
  182. checkinBtn.title = '左击开始签到/右击配置脚本';
  183. console.groupEnd(logName);
  184. },
  185.  
  186. /* @param {string} operationName 操作名称 */
  187. alterCheckinBtn = function(operationName) {
  188. checkinBtn.style.pointerEvents = 'none';
  189. Array.from(checkinBtn.querySelectorAll('em')).forEach((em) => {em.style.color = '#fa7d3c';});
  190. checkinBtn.querySelector('.signBtn').innerText = `${operationName}中…`;
  191. },
  192.  
  193. /* @param {boolean} auto 自动开始*/
  194. huatiCheckin = function(auto=true) {
  195. console.group(logName='微博超话签到');
  196.  
  197. /**
  198. * 获取关注话题列表
  199. * @param {object[]} [huatiList=[]] 关注话题列表
  200. * @param {string} huatiList[].name 名称
  201. * @param {string} huatiList[].hash 编号
  202. * @param {int|null} huatiList[].level 超话等级
  203. * @param {boolean} huatiList[].checked 超话已签
  204. * @param {string} [type='super'] 超话或普话, 'super'/'normal'
  205. * @param {int} [total=0] 关注话题数量
  206. * @param {int} [page=1] 列表页码
  207. */
  208. let getFollowList = function(huatiList=[], type='super', total=0, page=1) {
  209.  
  210. let getPage = new Task(
  211. `获取${type === 'super' ? '超' : '普'}话列表第${page}页`,
  212. {
  213. method: 'GET',
  214. url: `https://m.weibo.cn/api/container/getIndex?containerid=100803_-_page_my_follow_${type}&page=${page}`,
  215. },
  216. (xhr) => parsePage(xhr),
  217. () => getFollowList(huatiList, type, total, page)
  218. ),
  219.  
  220. parsePage = function(xhr) {
  221. let data = JSON.parse(xhr.responseText);
  222.  
  223. if (!data.cardlistInfo) {
  224. getPage.onerror('error');
  225. } else {
  226. if (page === 1)
  227. total += data.cardlistInfo.total;
  228.  
  229. data.cards[0].card_group.forEach(function(card) {
  230. if (card.card_type === 4) {
  231. let huati = {
  232. name: card.desc.slice(1, -1),
  233. level: type === 'super' ?
  234. +/level(\d+)\./.exec(card.icon)[1] : null,
  235. checked: !!card.avatar_url,
  236. hash: null,
  237. element: null
  238. };
  239.  
  240. if (lastHuatiList && lastHuatiList.includes(huati.name)) {
  241. if (!todayChecked) {
  242. Object.assign(huati, log.待签.find((huati_) => huati_.name === huati.name));
  243. if (huati.checked)
  244. Object.assign(huati, log.待签.splice(log.待签.findIndex((huati_) => huati_.name === huati.name), 1).pop());
  245. } else {
  246. huati.hash = log.已签[huati.name];
  247. huati.element = document.getElementById(`_${huati.hash}`);
  248. }
  249. } else {
  250. huati.hash = /100808(\w+)&/.exec(card.scheme)[1];
  251. huati.element = initElement(huati.name, huati.hash);
  252. }
  253. huatiList.push(huati);
  254.  
  255. if (huati.checked) {
  256. if (!lastHuatiList || (lastHuatiList.includes(huati.name) ? !todayChecked : true)) {
  257. checkinDone.appendChild(huati.element);
  258. initLog('已签', {});
  259. log.已签[huati.name] = huati.hash;
  260. }
  261. } else if (!lastHuatiList || (lastHuatiList.includes(huati.name) ? todayChecked && type === "super" : true)){
  262. checkinToDo.appendChild(huati.element);
  263. initLog('待签', []);
  264. log.待签.push(huati);
  265. }
  266. if (huati.level)
  267. setStatus(`Lv.${huati.level}`, huati.element);
  268. }
  269. });
  270.  
  271. if (huatiList.length < total)
  272. getFollowList(huatiList, type, total, page + 1);
  273. else if (config.checkNormal && type != 'normal')
  274. getFollowList(huatiList, 'normal', total);
  275. else {
  276. setStatus(`关注列表获取完毕,共${total}个${config.checkNormal ? '话题' : '超话'},` + (log.hasOwnProperty('待签') ? `${log.待签.length}个待签` : '全部已签'));
  277. console.table(huatiList);
  278. readyCheckin();
  279. }
  280. }
  281. };
  282. },
  283.  
  284. readyCheckin = function(){
  285. console.info(log);
  286.  
  287. if (log.hasOwnProperty('待签')) {
  288. if (config.autoCheckin)
  289. checkin(log.待签.shift());
  290. else {
  291. clearTask();
  292. /* 开始签到按钮 */
  293. let startCheckin = document.createElement('a');
  294. startCheckin.classList.add('S_ficon');
  295. startCheckin.onclick = () => checkin(log.待签.shift());
  296. startCheckin.innerText = '[开始签到]';
  297. checkinStatus.appendChild(startCheckin);
  298. }
  299. } else {
  300. clearTask();
  301. initCheckinBtn();
  302. }
  303. },
  304.  
  305. /* 获取话题编号 @param {array} list 话题名称列表 */
  306. getHash = function(list) {
  307. let name = list.shift(),
  308. huatiGetHash = new Task(
  309. `${name}话题信息获取`,
  310. {
  311. method: 'HEAD',
  312. url: `https://m.weibo.cn/api/container/getIndex?type=topic&value=${name}`,
  313. },
  314. (xhr) => {
  315. if (xhr.status === 200) {
  316. let regexp = /fid%3D100808(\w+)/g,
  317. hash = regexp.exec(xhr.responseHeaders.match(regexp).pop())[1];
  318. let element = initElement(name, hash);
  319. checkinToDo.append(element);
  320. initLog('待签', []);
  321. log.待签.push({name, hash, element});
  322. if (list.length)
  323. getHash(list);
  324. else {
  325. setStatus(`话题列表获取完毕,共${(log.hasOwnProperty('已签') ? Object.keys(log.已签).length : 0) + (log.hasOwnProperty('待签') ? log.待签.length : 0)}个话题` + (log.hasOwnProperty('待签') ? `${log.待签.length}个待签` : '全部已签'));
  326. readyCheckin();
  327. }
  328. }
  329. });
  330. },
  331.  
  332. getWhitelist = function() {
  333. let toDoList = whitelist.slice(0);
  334. if (!whitelist.length) {
  335. setStatus('尚未设置签到话题白名单!<a>[设置]</a>');
  336. checkinStatus.querySelector('a').onclick = () => {
  337. setupConfig();
  338. whitelistMode.click();
  339. editWhitelist.click();
  340. whitelistBox.focus();
  341. };
  342. clearTask();
  343. initCheckinBtn();
  344. } else {
  345. if (lastHuatiList) {
  346. for (let name of lastHuatiList) {
  347. if (!whitelist.includes(name)) {
  348. if (!todayChecked) {
  349. let index = log.待签.findIndex((huati) => huati.name === name);
  350. log.待签[index].element.remove();
  351. log.待签.splice(index, 1);
  352. }
  353. } else
  354. toDoList.splice(toDoList.indexOf(name), 1);
  355. }
  356. }
  357. if (toDoList.length)
  358. getHash(toDoList);
  359. else {
  360. setStatus(`话题列表获取完毕,共${(log.hasOwnProperty('已签') ? Object.keys(log.已签).length : 0) + (log.hasOwnProperty('待签') ? log.待签.length : 0)}个话题` + (log.hasOwnProperty('待签') ? `${log.待签.length}个待签` : '全部已签'));
  361. readyCheckin();
  362. }
  363. }
  364. },
  365.  
  366. /**
  367. * 话题签到
  368. * @param {object} huati 话题,参见 {@link getFollowList#huatiList}
  369. * @param {boolean} checkinAll 签到全部话题
  370. */
  371. checkin = function(huati, checkinAll=true) {
  372. let huatiCheckin = new Task(
  373. `${huati.name}话题签到`,
  374. {
  375. method: 'GET',
  376. url: `/p/aj/general/button?api=http://i.huati.weibo.com/aj/super/checkin&id=100808${huati.hash}`,
  377. },
  378. (xhr) => {
  379. let data = JSON.parse(xhr.responseText),
  380. code = +data.code;
  381.  
  382. switch (code) {
  383. case 100000:
  384. setStatus(`签到第${/\d+/g.exec(data.data.alert_title)[0]}名,经验+${/\d+/g.exec(data.data.alert_subtitle)[0]}`, huati.element, true);
  385. case 382004: {
  386. if (code != 100000)
  387. setStatus('已签', huati.element, true);
  388. checkinDone.appendChild(huati.element);
  389. initLog('已签', {});
  390. log.已签[huati.name] = huati.hash;
  391. Object.assign(lastCheckin, {date, nick: USER.NICK});
  392. Object.assign(lastCheckin, log.已签);
  393. GM_setValue(`lastCheckin${USER.UID}`, JSON.stringify(lastCheckin));
  394. break;
  395. }
  396. default: {
  397. setStatus(data.msg, huati.element, true);
  398. initLog('异常', {});
  399. log.异常[huati.name] = {huati, code: data.code, msg: data.msg, xhr: xhr};
  400. huatiCheckin.onerror('error');
  401. }
  402. }
  403. if (checkinAll) {
  404. if (log.待签.length > 0)
  405. checkin(log.待签.shift());
  406. else {
  407. clearTask();
  408. setStatus(`${date} 签到完成`);
  409. checkinToDo.parentNode.style.display = 'none';
  410. checkinDone.parentNode.setAttribute('open', '');
  411. Object.assign(lastCheckin, {allChecked: true});
  412. GM_setValue(`lastCheckin${USER.UID}`, JSON.stringify(lastCheckin));
  413. console.info(log);
  414. initCheckinBtn();
  415. if (config.autoCheckState)
  416. checkState();
  417. }
  418. }
  419. },
  420. () => checkin(huati, false),
  421. () => {
  422. log.待签.push(huati);
  423. if (log.待签.length > 0)
  424. checkin(log.待签.shift());
  425. else
  426. clearTask();
  427.  
  428. let retryHuati =document.createElement('a');
  429. retryHuati.classList.add('S_ficon');
  430. retryHuati.onclick = () => checkin(Object.assign({}, huati), false);
  431. retryHuati.innerText = '[重试]';
  432. setStatus(retryHuati, huati.element, true);
  433. }
  434. );
  435. },
  436.  
  437. initElement = function(name, hash) {
  438. /**
  439. * 文本限宽输出
  440. * @param {string} text 输入文本
  441. * @param {int} length 宽度限定
  442. * @return {string} 输出文本
  443. */
  444. let shorten = function(text, length) {
  445. let count = 0;
  446. for (let index in text) {
  447. let increment = /[\x00-\x7f]/.test(text[index]) ? 1 : 2;
  448. if (count + increment > length - 2)
  449. return `${text.substr(0, index)}…`;
  450. count += increment;
  451. }
  452. return text;
  453. },
  454. element = document.createElement('li');
  455. element.id = `_${hash}`;
  456. element.innerHTML = `<span class=order></span>.<a href=//weibo.com/p/100808${hash} target=_blank title=${name}>${shorten(name, 12)}</a><span class=info></span>`;
  457. return element;
  458. };
  459.  
  460. alterCheckinBtn('签到');
  461.  
  462. if (!lastCheckin.date || lastCheckin.date != date || !lastCheckin.allChecked || !auto) {
  463.  
  464. /* 设置信息展示界面 */
  465. let checkinCSS = document.getElementById('checkinCSS') || document.createElement('style');
  466. checkinCSS.id = 'checkinCSS';
  467. checkinCSS.type = 'text/css';
  468. 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;}`;
  469. document.head.appendChild(checkinCSS);
  470.  
  471. let checkinInfo = document.getElementById('checkinInfo') || document.createElement('div');
  472. checkinInfo.id = 'checkinInfo';
  473. checkinInfo.classList.add('W_layer');
  474. checkinInfo.innerHTML = `<div class=content><div><div id=checkinDetail><details open style=display:none><summary class="W_f14 W_fb">待签</summary><ol id=checkinToDo></ol></details><details style=display:none><summary class="W_f14 W_fb">已签</summary><ol id=checkinDone></ol></details></div></div><div class=W_layer_title>${USER.NICK}<span id=checkinStatus></span><a id=checkinMore title=${config.openDetail ? '收起' :'详情'} class="W_ficon S_ficon">${config.openDetail ? 'c' : 'd'}</a><a id=checkinClose title=${currentTask ? '中止' : '关闭'} class=W_ficon S_ficon>X</a></div></div>`;
  475. document.body.appendChild(checkinInfo);
  476.  
  477. checkinMore.onclick = function() {
  478. if (this.innerText === 'd') {
  479. this.innerText = 'c';
  480. this.title = '收起';
  481. checkinDetail.removeAttribute('style');
  482. } else {
  483. this.innerText = 'd';
  484. this.title = '详情';
  485. checkinDetail.style.display = 'none';
  486. }
  487. };
  488.  
  489. checkinClose.onclick = function() {
  490. if (currentTask) {
  491. currentTask.xhr.abort();
  492. setStatus(`${currentTask.name}中止`);
  493. clearTask();
  494. initCheckinBtn();
  495. } else {
  496. checkinInfo.remove();
  497. checkinCSS.remove();
  498. initCheckinBtn();
  499. }
  500. };
  501.  
  502. [checkinToDo, checkinDone].forEach((ol, i) =>
  503. ['DOMNodeInserted', 'DOMNodeRemoved'].forEach((event) =>
  504. ol.addEventListener(event, function() {
  505. let isRemoval = event != 'DOMNodeInserted',
  506. subtotal = ol.childElementCount - (isRemoval ? 1 : 0);
  507. if (this.parentNode.style.display === 'none')
  508. this.parentNode.removeAttribute('style');
  509. this.previousSibling.innerText = `${i ? '已' : '待'}签${subtotal}个话题`;
  510. Array.from(this.querySelectorAll('li .order')).forEach((span) => /* 计算序号并按小计添加 en quad 进行格式化 */
  511. span.innerText = (Array.from(span.parentNode.parentNode.querySelectorAll('li')).findIndex((li) =>
  512. li === span.parentNode) + (isRemoval ? 0 : 1)).toString().padStart(subtotal.toString().length).replace(/ /g, String.fromCharCode(8192)));
  513. })));
  514.  
  515. /* 开始获取话题列表 */
  516. checkinClose.title = '中止';
  517.  
  518. if (lastCheckin.date) {
  519. setStatus(`从${lastCheckin.date}签到记录读取话题列表`);
  520. var lastHuatiList = [],
  521. todayChecked = lastCheckin.date === date;
  522. for (let name in lastCheckin) {
  523. if (!['date', 'nick', 'allChecked'].includes(name)) {
  524. lastHuatiList.push(name);
  525. let hash = lastCheckin[name],
  526. element = initElement(name, hash);
  527. if (!todayChecked) {
  528. checkinToDo.appendChild(element);
  529. initLog('待签', []);
  530. log.待签.push({name, hash, element});
  531. } else {
  532. checkinDone.appendChild(element);
  533. initLog('已签', {});
  534. log.已签[name] = hash;
  535. }
  536. }
  537. }
  538. if (!todayChecked)
  539. lastCheckin = {};
  540. if (log.hasOwnProperty('待签') && log.待签.length) {
  541. setStatus(`话题列表读取完毕,共${log.待签.length}个话题待签`);
  542. if (config.checkinMode === 'followListMode') {
  543. if (config.autoCheckin)
  544. checkin(log.待签.shift());
  545. else {
  546. /* 开始签到按钮 */
  547. let startCheckin = document.createElement('a');
  548. startCheckin.classList.add('S_ficon');
  549. startCheckin.onclick = () => checkin(log.待签.shift());
  550. startCheckin.innerText = '[开始签到]';
  551. checkinStatus.appendChild(startCheckin);
  552. }
  553. }
  554. }
  555. }
  556. switch (config.checkinMode) {
  557. case 'followListMode':
  558. getFollowList();
  559. break;
  560. case 'whitelistMode':
  561. getWhitelist();
  562. break;
  563. }
  564. } else
  565. initCheckinBtn();
  566. },
  567.  
  568. importWhitelist = () => Object.keys(lastCheckin).filter((key) => !['date', 'nick', 'allChecked'].includes(key)),
  569.  
  570. checkState = function(list=importWhitelist(), manageList=[]) {
  571. if (!arguments.length) {
  572. console.group('话题状态查询');
  573. alterCheckinBtn('查询');
  574. }
  575. let load = (xhr, name, hash, manage=false) => {
  576. try {
  577. let data = JSON.parse(manage ? xhr.responseText.slice(1, -1) : xhr.responseText),
  578. element = document.getElementById(`_${hash}`);
  579. if (!data.ok) {
  580. (manage ? manageList : list).push(name);
  581. } else if (manage) {
  582. setStatus(';' + data.card_group[1].group.map(
  583. (item) => item.item_desc + item.item_title).join(), element, true);
  584. } else {
  585. if (Object.keys(data.cardlistInfo.button).length)
  586. manageList.push(name);
  587. let cards = data.cards;
  588. setStatus((
  589. 11 != +cards[1].card_type ? '' : cards[1].card_group.reduce(
  590. (text, card) => 4 != +card.card_type || !/\d/.test(card.desc) ? text : text +
  591. card.desc.replace(/[^\.\d]*(\.?\d+)\D.*/g,
  592. (_, match) => (match.includes('.') ? 'Lv' : '-') + match),
  593. '')), element);
  594. element.title = cards[0].group.map(
  595. (item) => item.item_title + item.item_desc).join() + (!cards[3] || 4 != cards[3].card_type ? '' : ',' + cards[3].desc.replace('超级话题', ''));
  596. }
  597. } catch (e) {
  598. console.error(e);
  599. (manage ? manageList : list).push(name);
  600. }
  601. checkState(list, manageList);
  602. };
  603. if (!list.length) {
  604. if (!manageList.length) {
  605. setStatus('查询完毕。');
  606. clearTask();
  607. initCheckinBtn();
  608. console.groupEnd('话题状态查询');
  609. } else {
  610. let name = manageList.shift(),
  611. hash = lastCheckin[name],
  612. stateCheck = new Task(
  613. `查询${name}话题考核进度`,
  614. {
  615. method: 'GET',
  616. url: `https://m.weibo.cn/api/container/getItem?itemid=100808${hash}_-_assess_progress`,
  617. },
  618. (xhr) => load(xhr, name, hash, true)
  619. );
  620. }
  621. } else {
  622. let name = list.shift(),
  623. hash = lastCheckin[name],
  624. stateCheck = new Task(
  625. `查询${name}话题状态`,
  626. {
  627. method: 'GET',
  628. url: `https://m.weibo.cn/api/container/getIndex?containerid=231140${hash}_-_super_detail`,
  629. },
  630. (xhr) => load(xhr, name, hash)
  631. );
  632. }
  633. },
  634.  
  635. setupConfig = function() {
  636. console.group(logName='微博超话签到设置');
  637. alterCheckinBtn('设置');
  638.  
  639. let configCSS = document.createElement('style');
  640. configCSS.id = 'configCSS';
  641. configCSS.type = 'text/css';
  642. configCSS.innerHTML = `#configForm {z-index:6666;position:fixed;right: 0px;top: 50px;width:540px;opacity: 0.9}#configForm a {cursor: pointer}#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}
  643. #configForm fieldset > fieldset > legend {text-align: right; padding:3px}#configForm input[type=number] {width: 48px}#configForm input[type=button] {padding: 0 12px}#configForm th {font-weight: bold;padding: 6px 0 3px}#configForm table {float: left;margin: 0 6px}#configForm div {padding: 6px;height: 160px;overflow-y: scroll;background-color: whitesmoke;line-height: 1.5}${scrollbarStyle('#configForm textarea', '#configForm div')}#configForm span {float: right; margin-top: 3px}
  644. #configForm textarea {width: 120px; height: 90px;padding: 6px;margin: 6px 0}`;
  645. document.head.appendChild(configCSS);
  646.  
  647. let configForm = document.createElement('div');
  648. configForm.id = 'configForm';
  649. configForm.classList.add('W_layer');
  650. configForm.innerHTML = `<form class=content><header class=W_layer_title><img src=//img.t.sinajs.cn/t6/style/images/pagecard/icon.png>签到脚本设置<a title=关闭 class=W_ficon S_ficon id=configClose>X</a></header><main><fieldset><legend>参数设定</legend>
  651. <fieldset><legend>签到模式</legend><label for=followListMode title=先获取话题关注列表再进行签到><input id=followListMode type=radio value=followListMode name=checkinMode>关注列表模式  <label for=checkNormal><input type=checkbox id=checkNormal for=followListMode name=checkinModeSub>普话签到</label></label><br>
  652. <label for=whitelistMode title=只读取本地名单并按顺序签到><input id=whitelistMode type=radio value=whitelistMode name=checkinMode>白名单模式  <input type=button id=editWhitelist for=whitelistMode name=checkinModeSub value=编辑名单></label></fieldset>
  653. <fieldset><legend>运行参数</legend>请求延时 <input type=number id=delay min=0 max=1000 step=100> 毫秒<span><label for=autoCheckin><input type=checkbox id=autoCheckin>自动签到</label><br><label for=autoCheckState title=自动查询等级、连续签到天数、话题数据及主持人考核进度><input type=checkbox id=autoCheckState>自动查询</label></span><br>请求超时 <input type=number id=timeout min=1000 max=10000 step=100> 毫秒<br>自动重试 <input type=number id=retry min=0 max=10> 次</fieldset>
  654. <fieldset><legend>签到详情</legend>最大高度 <input type=number id=maxHeight min=60 max=1080 step=60> 像素<span><label for=openDetail><input type=checkbox id=openDetail>自动展开</label></span></fieldset>
  655. </fieldset>
  656. <fieldset id=accountInfo><legend>账户信息</legend><table><tbody><tr><th>昵称</th></tr><tr><td>${USER.NICK}</td></tr><tr><th>ID</th></tr><tr><td>${USER.UID}</td></tr><tr><th>上次签到</th></tr><tr><td>${lastCheckin.date || '尚无记录'}</td></tr><tr><th><input type=button value=状态查询 id=stateCheck></th></tr><tr><th><input type=button value=清空记录 id=configClear ${Object.keys(lastCheckin).length != 0 ? '' : 'disabled'}></th></tr></tbody></table><div>${importWhitelist().map((name) => {
  657. let hash = lastCheckin[name];
  658. return '<p id=_' + hash + '><a href=//weibo.com/p/100808' + hash + ' target=_blank>' + name + '</a><i class=info></i></p>';
  659. }).join('')}</div></fieldset>
  660. <fieldset id=whitelistEditor style=display:none><legend>签到名单编辑</legend>请在下方编辑名单,每行一个话题名,完成后点击[保存名单]按钮。<br><textarea id=whitelistBox placeholder="每行一个话题名,不带#号,如\n读书\n美食">${whitelist.join('\n')}</textarea><span><input type=button id=whitelistSave value=保存名单 disabled><input type=button id=whitelistImport value=导入列表 title=导入签到记录中的话题列表></fieldset>
  661. <footer><input type=button value=保存 id=configSave disabled><input type=button value=还原 id=configRestore disabled><input type=button value=重置 id=configDefault><span><a href=//greasyfork.org/scripts/32143/feedback target=_blank>GreasyFork</a> / <a href=//gist.github.com/xyauhideto/b9397058ca3166b87e706cbb7249bd54 target=_blank>Gist</a> / <a href=//weibo.com/678896489 target=_blank>微博</a> 报错请F12提供后台记录</span></footer> </form>`;
  662. document.body.appendChild(configForm);
  663. let inputs = Array.from(configForm.querySelectorAll('input:not([type=button])')),
  664.  
  665. getWhitelist = () => whitelistBox.value.split('\n').filter((name) => name.trim().length),
  666. getInputs = () => inputs.reduce((conf, input) => {
  667. if (!(input.type === 'radio' && !input.checked))
  668. conf[input.type != 'radio' ? input.id : input.name] = input.type != 'number' ? input.type != 'checkbox' ? input.value : input.checked : Math.max(+input.min, Math.min(+input.max, +input.value));
  669. // console.log(conf);
  670. return conf;
  671. }, {}),
  672.  
  673. initForm = function(conf=config) {
  674. for (let [key, value] of Object.entries(conf)) {
  675. let input = document.getElementById(key) || document.querySelector(`[name=${key}][value=${value}]`);
  676. if (typeof value === 'boolean')
  677. input.checked = value;
  678. else if (typeof value === 'string') {
  679. input.checked = true;
  680. document.querySelector(`input[for=${value}]`).removeAttribute('disabled');
  681. let other = document.querySelector(`[name=${key}Sub]:not([for=${value}])`);
  682. if (other.value === '退出编辑')
  683. other.click();
  684. other.disabled = true;
  685. } else
  686. input.value = value;
  687. }
  688. configRestore.disabled = isEqual(conf, config);
  689. configDefault.disabled = isEqual(conf, DEFAULT_CONFIG);
  690. configSave.disabled = configRestore.disabled;
  691. whitelistBox.oninput();
  692. },
  693.  
  694. /**
  695. * 简单对象、阵列比较
  696. * @param {object|array} x 比较对象/阵列x
  697. * @param {object|array} y 比较对象/阵列y
  698. * @return {boolean} 比较结果
  699. */
  700. isEqual = function(x, y) {
  701. if (Object.values(x).length != Object.values(y).length)
  702. return false;
  703. if (x instanceof Array) {
  704. for (let value of x) {
  705. if (!y.includes(value))
  706. return false;
  707. }
  708. } else {
  709. for (let key in x) {
  710. if (!y.hasOwnProperty(key) || x[key] != y[key])
  711. return false;
  712. }
  713. }
  714. return true;
  715. };
  716.  
  717. stateCheck.onclick = ()=>checkState();
  718.  
  719. configSave.onclick = function() {
  720. config = getInputs();
  721. if (!whitelistSave.disabled && confirm('尚未保存签到名单,一起保存?'))
  722. whitelistSave.click();
  723. if (editWhitelist.value === '退出编辑')
  724. editWhitelist.click();
  725. GM_setValue(`config${USER.UID}`, JSON.stringify(config));
  726. initForm();
  727. };
  728. configRestore.onclick = () => initForm();
  729. configDefault.onclick = function() {
  730. GM_deleteValue(`config${USER.UID}`);
  731. initForm(DEFAULT_CONFIG);
  732. };
  733. configClear.onclick = function() {
  734. console.warn('清空上次签到');
  735. console.table(lastCheckin);
  736. GM_deleteValue(`lastCheckin${USER.UID}`);
  737. lastCheckin = {};
  738. document.querySelector('#configForm tr:nth-of-type(6)>td').innerText = '尚无记录';
  739. document.querySelector('#configForm div').innerText = '';
  740. this.disabled = true;
  741. };
  742. configClose.onclick = function() {
  743. configCSS.remove();
  744. configForm.remove();
  745. initCheckinBtn();
  746. };
  747.  
  748. inputs.forEach(function(input) {
  749. input.onchange = () => initForm(getInputs());
  750. if (input.parentNode.title) {
  751. input.onfocus = () => {
  752. let tip = document.createElement('i');
  753. tip.innerText = input.parentNode.title;
  754. tip.style = `position:absolute;left:${input.offsetLeft - 10 * input.parentNode.title.length}px;top:${input.offsetTop + 15}px;padding:3px;border:1px solid grey;color:grey;background-color:white;box-shadow:1px 1px 2px`;
  755. input.parentNode.append(tip);
  756. };
  757. input.onblur = () => input.parentNode.lastChild.remove();
  758. }
  759. });
  760.  
  761. editWhitelist.onclick = function() {
  762. if (this.value === '编辑名单') {
  763. whitelistBox.value = whitelist.join('\n');
  764. accountInfo.style.display = 'none';
  765. whitelistEditor.removeAttribute('style');
  766. this.value = '退出编辑';
  767. } else {
  768. whitelistEditor.style.display = 'none';
  769. accountInfo.removeAttribute('style');
  770. this.value = '编辑名单';
  771. }
  772. };
  773. whitelistSave.onclick = function() {
  774. let whitelist_ = getWhitelist();
  775. if (whitelist_.length || confirm('尚未设定白名单,继续保存?')) {
  776. whitelist = whitelist_;
  777. GM_setValue(`whitelist${USER.UID}`, JSON.stringify(whitelist));
  778. whitelistEditor.style.display = 'none';
  779. accountInfo.removeAttribute('style');
  780. editWhitelist.value = '编辑名单';
  781. }
  782. };
  783. whitelistImport.onclick = function() {
  784. whitelistBox.value = importWhitelist().join('\n');
  785. whitelistBox.oninput();
  786. };
  787. whitelistBox.oninput = function() {
  788. let whitelist_ = getWhitelist();
  789. whitelistSave.disabled = isEqual(Object.assign({}, whitelist_), Object.assign({}, whitelist));
  790. whitelistImport.disabled = isEqual(whitelist_, importWhitelist());
  791. };
  792.  
  793. initForm();
  794. },
  795.  
  796. /**
  797. * 提示签到状态
  798. * @param {string|node} status 当前状态
  799. * @param {node} [element=checkinStatus] 显示提示的节点
  800. * @param {boolean} [append=false] 追加节点
  801. */
  802. setStatus = function(status, element=document.getElementById('checkinStatus'), append=false) {
  803. try {
  804. if (element.id && element.id.startsWith('_'))
  805. element = element.querySelector('.info');
  806. else if (!append)
  807. console.info(status);
  808.  
  809. if (append) {
  810. if (typeof status != 'string')
  811. element.appendChild(status);
  812. else
  813. element.innerHTML += status;
  814. } else
  815. element.innerHTML = status;
  816. } catch (e) {
  817. console.error(e);
  818. }
  819. },
  820.  
  821. scrollbarStyle = function() {
  822. return Array.from(arguments).map((elementSelector) => `${elementSelector}::-webkit-scrollbar {width: 4px;background-color: #f2f2f5;border-radius: 2px;}${elementSelector}::-webkit-scrollbar-thumb {width: 4px;background-color: #808080;border-radius: 2px;}`).join('');
  823. },
  824.  
  825. /* 隐藏游戏按钮,替换为超话签到 */
  826. checkinBtn = document.createElement("li");
  827. checkinBtn.id = 'checkinBtn';
  828. checkinBtn.innerHTML = `<a><em class="W_ficon ficon_checkin S_ficon">s</em><em class="S_txt1 signBtn">超话签到</em></a>`;
  829. checkinBtn.addEventListener('contextmenu', () => setupConfig(), true);
  830. checkinBtn.addEventListener('click', () => huatiCheckin(false), true);
  831.  
  832. let navLast = document.querySelector('.gn_nav_list li:last-child');
  833. navLast.parentNode.insertBefore(checkinBtn, navLast);
  834. document.querySelector('a[nm=game]').parentNode.style.display = 'none';
  835.  
  836. /* 清理旧版数据 */
  837. ['autoSignbox', 'todaySigned'].forEach((key) => GM_deleteValue(key));
  838.  
  839. /* 自动签到 */
  840. if (config.autoCheckin)
  841. huatiCheckin();
  842. }});