批量撤回评测点赞

批量撤回评测点赞/有趣

目前为 2022-03-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name:zh-CN 批量撤回评测点赞
  3. // @name Recommend_Unrate
  4. // @namespace https://blog.chrxw.com
  5. // @supportURL https://blog.chrxw.com/scripts.html
  6. // @contributionURL https://afdian.net/@chr233
  7. // @version 1.2
  8. // @description 批量撤回评测点赞/有趣
  9. // @description:zh-CN 批量撤回评测点赞/有趣
  10. // @author Chr_
  11. // @match https://help.steampowered.com/zh-cn/accountdata/GameReviewVotesAndTags
  12. // @connect steamcommunity.com
  13. // @license AGPL-3.0
  14. // @icon https://blog.chrxw.com/favicon.ico
  15. // @grant GM_addStyle
  16. // @grant GM_xmlhttpRequest
  17. // ==/UserScript==
  18.  
  19.  
  20. (() => {
  21. "use strict";
  22.  
  23. const defaultRules = [
  24. "$$⠄|⢁|⠁|⣀|⣄|⣤|⣆|⣦|⣶|⣷|⣿|⣇|⣧",
  25. "pan",
  26. "share",
  27. "weiyun",
  28. "lanzou",
  29. "baidu",
  30. ].join("\n");
  31.  
  32. const rateTable = document.getElementById("AccountDataTable_1");
  33. const tagTable = document.getElementById("AccountDataTable_2");
  34. const hideArea = document.createElement("div");
  35. const banner = document.querySelector(".feature_banner");
  36. const describe = document.createElement("div");
  37. const { script: { version } } = GM_info;
  38. describe.innerHTML = `
  39. <h4>批量撤回评测点赞 Ver ${version} By 【<a href="https://steamcommunity.com/id/Chr_" target="_blank">Chr_</a>】</h4>
  40. <h5>关键词黑名单设置: 【<a href="#" class="ru_default">重置</a>】</h5>
  41. <p> 1. 仅会对含有黑名单词汇的评测消赞</p>
  42. <p> 2. 一行一条规则, 支持 * ? 作为通配符</p>
  43. <p> 3. Steam 评测是社区的重要组成部分, 请尽量使用黑名单进行消赞</p>
  44. <p> 4. 一些常用的规则参见 【<a href="https://keylol.com/t794532-1-1" target="_blank">发布帖</a>】</p>
  45. <p> 5. 如果需要使用正则表达式, 请以 $$ 开头</p>
  46. <p> 6. 如果需要对所有评测消赞, 请填入 * </p>`;
  47. banner.appendChild(describe);
  48. const filter = document.createElement('textarea');
  49. filter.placeholder = "黑名单规则, 一行一条, 支持 * ? 作为通配符";
  50. filter.className = "ru_filter";
  51. filter.value = window.localStorage.getItem("ru_rules") ?? defaultRules;
  52. const resetRule = banner.querySelector(".ru_default");
  53. resetRule.onclick = () => {
  54. ShowConfirmDialog(`⚠️操作确认`, `<div>确定要重置规则吗?</div>`, '确认', '取消')
  55. .done(() => { filter.value = defaultRules; })
  56. .fail(() => {
  57. const dialog = ShowDialog("操作已取消");
  58. setTimeout(() => { dialog.Dismiss(); }, 1000);
  59. });
  60. }
  61. banner.appendChild(filter);
  62. hideArea.style.display = "none";
  63. function genBtn(ele) {
  64. const b = document.createElement("button");
  65. b.innerText = "执行消赞";
  66. b.className = "ru_btn";
  67. b.onclick = async () => {
  68. b.disabled = true;
  69. b.innerText = "执行中...";
  70. await comfirmUnvote(rateTable);
  71. b.disabled = false;
  72. b.innerText = "执行消赞";
  73. };
  74. return b;
  75. }
  76. rateTable.querySelector("thead>tr>th:nth-child(1)").appendChild(genBtn(rateTable));
  77. tagTable.querySelector("thead>tr>th:nth-child(1)").appendChild(genBtn(tagTable));
  78. window.addEventListener("beforeunload", () => { window.localStorage.setItem("ru_rules", filter.value); });
  79.  
  80. // 操作确认
  81. async function comfirmUnvote(ele) {
  82. ShowConfirmDialog(`⚠️操作确认`, `<div>即将开始进行批量消赞, 强制刷新页面可以随时中断操作</div>`, '开始消赞', '取消')
  83. .done(() => { doUnvote(ele); })
  84. .fail(() => {
  85. const dialog = ShowDialog("操作已取消");
  86. setTimeout(() => { dialog.Dismiss(); }, 1000);
  87. });
  88. }
  89. // 执行消赞
  90. async function doUnvote(ele) {
  91. // 获取所有规则并去重
  92. const rules = filter.value.split("\n").map(x => x)
  93. .filter((item, index, arr) => item && arr.indexOf(item, 0) === index)
  94. .map((x) => {
  95. if (x.startsWith("$$")) {
  96. try {
  97. return [2, new RegExp(x.slice(2), "ig")];
  98. } catch (e) {
  99. ShowDialog("正则表达式有误", x);
  100. return [-1, null];
  101. }
  102. }
  103. else if (x.includes("*") || x.includes("?")) {
  104. return [1, x];
  105. }
  106. return [0, x];
  107. });
  108. const [, sessionID] = await fetchSessionID();
  109. const rows = ele.querySelectorAll("tbody>tr");
  110.  
  111. for (const row of rows) {
  112. if (row.className.includes("ru_opt") || row.childNodes.length !== 4) {
  113. continue;
  114. }
  115. const [name, , , link] = row.childNodes;
  116. const url = link.childNodes[0].href;
  117. const [succ, recomment, id, rate] = await fetchRecommended(url);
  118.  
  119. if (!succ) {//读取评测失败
  120. name.innerText += `【⚠️${recomment}】`;
  121. row.className += " ru_opt";
  122. continue;
  123. }
  124.  
  125. let flag = false;
  126. for (const [mode, rule] of rules) {
  127. if (mode === 2) {// 正则模式
  128. if (recomment.search(rule) !== -1) {
  129. flag = true;
  130. break;
  131. }
  132. } else if (mode === 1) {//简易通配符
  133. if (isMatch(recomment, rule)) {
  134. flag = true;
  135. break;
  136. }
  137. } else if (mode === 0) { //关键字搜寻
  138. if (recomment.includes(rule)) {
  139. flag = true;
  140. break;
  141. }
  142. }
  143. }
  144. if (flag) {//需要消赞
  145. const raw = name.innerText;
  146. name.innerText = `${raw}【❌ 命中规则】`;
  147. const succ1 = await changeVote(id, true, sessionID);
  148. const succ2 = await changeVote(id, false, sessionID);
  149.  
  150. if (succ1 && succ2) {
  151. name.innerText = `${raw}【💔 消赞成功】`;
  152. } else {
  153. name.innerText = `${raw}【💥 消赞失败(请检查社区是否登陆)】`;
  154. }
  155. }
  156. else {
  157. name.innerText += "【💚 无需消赞】";
  158. }
  159. row.className += " ru_opt";
  160. }
  161. }
  162. // 获取SessionID
  163. function fetchSessionID() {
  164. return new Promise((resolve, reject) => {
  165. $http.getText("https://steamcommunity.com/id/Chr_/")
  166. .then((text) => {
  167. const sid = (text.match(/g_sessionID = "(.+)";/) ?? ["", ""])[1];
  168. resolve([sid !== "", sid]);
  169. }).catch((err) => {
  170. console.error(err);
  171. resolve([false, ""]);
  172. });
  173. });
  174. }
  175. // 获取评测详情
  176. // 返回 (状态, 评测内容, id , rate)
  177. function fetchRecommended(url) {
  178. return new Promise((resolve, reject) => {
  179. $http.getText(url)
  180. .then((text) => {
  181. const area = document.createElement("div");
  182. hideArea.appendChild(area);
  183. area.innerHTML = text;
  184. const recomment = area.querySelector("#ReviewText")?.innerText.trim() ?? "获取失败";
  185. const eleVoteUp = area.querySelector("span[id^='RecommendationVoteUpBtn']");
  186. const voteUp = eleVoteUp?.className.includes("btn_active");
  187. const voteDown = area.querySelector("span[id^='RecommendationVoteDownBtn']")?.className.includes("btn_active");
  188. const voteTag = area.querySelector("span[id^='RecommendationVoteTagBtn']")?.className.includes("btn_active");
  189. const recommentID = eleVoteUp ? parseInt(eleVoteUp.id.replace("RecommendationVoteUpBtn", "")) : 0;
  190. // 好评=1 差评=2 欢乐=3 未评价=0 解析失败=-1
  191. const rate = voteUp ? 1 : voteDown ? 2 : voteTag ? 3 : (voteUp == null || voteDown == null || voteTag == null) ? -1 : 0;
  192. hideArea.removeChild(area);
  193. resolve([true, recomment, recommentID, rate]);
  194. }).catch((err) => {
  195. console.error(err);
  196. resolve([false, "未知错误 :" + err, 0, 0]);
  197. });
  198. });
  199. }
  200. // 进行消赞
  201. function changeVote(recID, state, sessionid) {
  202. return new Promise((resolve, reject) => {
  203. let data = `tagid=1&rateup=${state}&sessionid=${sessionid}`;
  204. $http.post(`https://steamcommunity.com/userreviews/votetag/${recID}`, data, {
  205. headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
  206. })
  207. .then((json) => {
  208. const { success } = json;
  209. resolve(success === 1);
  210. }).catch((err) => {
  211. console.error(err);
  212. resolve(false);
  213. });
  214. });
  215. }
  216. // 通配符匹配
  217. function isMatch(string, pattern) {
  218. let dp = [];
  219. for (let i = 0; i <= string.length; i++) {
  220. let child = [];
  221. for (let j = 0; j <= pattern.length; j++) {
  222. child.push(false);
  223. }
  224. dp.push(child);
  225. }
  226. dp[string.length][pattern.length] = true;
  227. for (let i = pattern.length - 1; i >= 0; i--) {
  228. if (pattern[i] != "*") {
  229. break;
  230. } else {
  231. dp[string.length][i] = true;
  232. }
  233. }
  234. for (let i = string.length - 1; i >= 0; i--) {
  235. for (let j = pattern.length - 1; j >= 0; j--) {
  236. if (string[i] == pattern[j] || pattern[j] == "?") {
  237. dp[i][j] = dp[i + 1][j + 1];
  238. } else if (pattern[j] == "*") {
  239. dp[i][j] = dp[i + 1][j] || dp[i][j + 1];
  240. } else {
  241. dp[i][j] = false;
  242. }
  243. }
  244. }
  245. return dp[0][0];
  246. };
  247. class Request {
  248. 'use strict';
  249. constructor(timeout = 3000) {
  250. this.timeout = timeout;
  251. }
  252. get(url, opt = {}) {
  253. return this.#baseRequest(url, 'GET', opt, 'json');
  254. }
  255. getText(url, opt = {}) {
  256. return this.#baseRequest(url, 'GET', opt, 'text');
  257. }
  258. post(url, data, opt = {}) {
  259. opt.data = data;
  260. return this.#baseRequest(url, 'POST', opt, 'json');
  261. }
  262. #baseRequest(url, method = 'GET', opt = {}, responseType = 'json') {
  263. Object.assign(opt, {
  264. url, method, responseType, timeout: this.timeout
  265. });
  266. return new Promise((resolve, reject) => {
  267. opt.ontimeout = opt.onerror = reject;
  268. opt.onload = ({ readyState, status, response, responseXML, responseText }) => {
  269. if (readyState === 4 && status === 200) {
  270. if (responseType === 'json') {
  271. resolve(response);
  272. } else if (responseType === 'text') {
  273. resolve(responseText);
  274. } else {
  275. resolve(responseXML);
  276. }
  277. } else {
  278. console.error('网络错误');
  279. console.log(readyState);
  280. console.log(status);
  281. console.log(response);
  282. reject('解析出错');
  283. }
  284. }
  285. GM_xmlhttpRequest(opt);
  286. });
  287. }
  288. }
  289. const $http = new Request();
  290.  
  291. GM_addStyle(`
  292. .feature_banner {
  293. background-size: cover;
  294. }
  295. .feature_banner > div{
  296. margin-left: 10px;
  297. color: #fff;
  298. font-weight: 200;
  299. }
  300. .ru_btn {
  301. margin-left: 5px;
  302. padding: 2px;
  303. }
  304. .ru_filter {
  305. resize: vertical;
  306. width: calc(100% - 30px);
  307. min-height: 80px;
  308. margin: 10px;
  309. }
  310. `);
  311. })();
  312.