批量撤回评测点赞

批量撤回评测点赞/有趣

当前为 2022-03-06 提交的版本,查看 最新版本

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