批量撤回评测点赞

批量撤回评测点赞/有趣

安装此脚本?
作者推荐脚本

您可能也喜欢Steam显示英文游戏名

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址