Weibo Huati Check-in

超级话题集中签到

当前为 2018-05-31 提交的版本,查看 最新版本

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