批量撤回评测点赞/有趣
// ==UserScript== // @name:zh-CN 批量撤回评测点赞 // @name Recommend_Unrate // @namespace https://blog.chrxw.com // @supportURL https://blog.chrxw.com/scripts.html // @contributionURL https://afdian.com/@chr233 // @version 1.12 // @description 批量撤回评测点赞/有趣 // @description:zh-CN 批量撤回评测点赞/有趣 // @author Chr_ // @match https://help.steampowered.com/zh-cn/accountdata/GameReviewVotesAndTags // @connect steamcommunity.com // @license AGPL-3.0 // @icon https://blog.chrxw.com/favicon.ico // @grant GM_addStyle // @grant GM_xmlhttpRequest // ==/UserScript== (() => { "use strict"; const defaultRules = [ "$$⠄|⢁|⠁|⣀|⣄|⣤|⣆|⣦|⣶|⣷|⣿|⣇|⣧", "$$我是((伞兵|傻|啥|煞|聪明|s)|(比|逼|币|b))", "$$(补|布)丁|和谐|去兔子", "$$度盘|网盘|链接|提取码", "$$步兵|骑兵", "$$pan|share|weiyun|lanzou|baidu", "链接已删除", "steam://install", "/s/", ].join("\n"); const rateTable = document.getElementById("AccountDataTable_1"); const tagTable = document.getElementById("AccountDataTable_2"); const hideArea = document.createElement("div"); const banner = document.querySelector(".feature_banner"); const describe = document.createElement("div"); const { script: { version } } = GM_info; describe.innerHTML = ` <h4>批量撤回评测点赞 Ver ${version} By 【<a href="https://steamcommunity.com/id/Chr_" target="_blank">Chr_</a>】</h4> <h5>关键词黑名单设置: 【<a href="#" class="ru_default">重置规则</a>】</h5> <p> 1. 仅会对含有黑名单词汇的评测消赞</p> <p> 2. 一行一条规则, 默认为关键词模式 (评测中需要出现指定的词汇才会判断为需要消赞)</p> <p> 3. 以 !! 开头的规则为简易通配符模式 (比如 !!我是?? 可以匹配包含 我是xx 的评测)</p> <p> 4. 以 $$ 开头的规则为正则表达式模式 (比如 $$我是([啥s]|[比b]) 可以匹配包含 我是sb 的评测</p> <p> 5. 以 # 开头的规则将被视为注释, 不会生效</p> <p> 6. <b>Steam 评测是社区的重要组成部分, 请尽量使用黑名单进行消赞</b></p> <p> 7. 一些常用的规则参见 【<a href="https://keylol.com/t794532-1-1" target="_blank">发布帖</a>】</p> <p> 8. 如果需要对所有评测消赞, 请填入 !!* </p>`; banner.appendChild(describe); const filter = document.createElement('textarea'); filter.placeholder = "黑名单规则, 一行一条, 支持 * ? 作为通配符, 支持正则表达式"; filter.className = "ru_filter"; const savedRules = window.localStorage.getItem("ru_rules"); filter.value = savedRules !== null ? savedRules : defaultRules; const resetRule = banner.querySelector(".ru_default"); resetRule.onclick = () => { ShowConfirmDialog(`⚠️操作确认`, `<div>确定要重置规则吗?</div>`, '确认', '取消') .done(() => { filter.value = defaultRules; }) .fail(() => { const dialog = ShowDialog("操作已取消"); setTimeout(() => { dialog.Dismiss(); }, 1000); }); }; banner.appendChild(filter); hideArea.style.display = "none"; function genBtn(ele) { const b = document.createElement("button"); b.innerText = "执行消赞"; b.className = "ru_btn"; b.onclick = async () => { b.disabled = true; b.innerText = "执行中..."; await comfirmUnvote(ele); b.disabled = false; b.innerText = "执行消赞"; }; return b; } rateTable.querySelector("thead>tr>th:nth-child(1)").appendChild(genBtn(rateTable)); tagTable.querySelector("thead>tr>th:nth-child(1)").appendChild(genBtn(tagTable)); window.addEventListener("beforeunload", () => { window.localStorage.setItem("ru_rules", filter.value); }); // 操作确认 async function comfirmUnvote(ele) { ShowConfirmDialog(`⚠️操作确认`, `<div>即将开始进行批量消赞, 强制刷新页面可以随时中断操作</div>`, '开始消赞', '取消') .done(() => { doUnvote(ele); }) .fail(() => { const dialog = ShowDialog("操作已取消"); setTimeout(() => { dialog.Dismiss(); }, 1000); }); } // 执行消赞 async function doUnvote(ele) { // 获取所有规则并去重 const rules = filter.value.split("\n").map(x => x) .filter((item, index, arr) => item && arr.indexOf(item, 0) === index) .map((x) => { if (x.startsWith("#")) { return [0, x]; } else if (x.startsWith("$$")) { try { return [2, new RegExp(x.substring(2), "ig")]; } catch (e) { ShowDialog("正则表达式有误", x); return [-1, null]; } } else if (x.startsWith("!!")) { return [1, x.substring(2).replace(/\*+/g, '*')]; } else if (x.includes("*") || x.includes("?")) { return [1, x.replace(/\*+/g, '*')]; } return [0, x]; }); const [, sessionID] = await fetchSessionID(); const rows = ele.querySelectorAll("tbody>tr"); for (const row of rows) { if (row.className.includes("ru_opt") || row.childNodes.length !== 4) { continue; } const [name, , , link] = row.childNodes; const url = link.childNodes[0].href; const [succ, recomment, id, rate] = await fetchRecommended(url); if (!succ) {//读取评测失败 name.innerText += `【⚠️${recomment}】`; row.className += " ru_opt"; continue; } let flag = false; let txt = ""; for (const [mode, rule] of rules) { if (mode === 2) {// 正则模式 if (recomment.search(rule) !== -1) { flag = true; txt = rule.toString().substring(0, 8); break; } } else if (mode === 1) {//简易通配符 if (isMatch(recomment.replace(/\?|\*/g, ""), rule)) { flag = true; txt = rule.substring(0, 8); break; } } else if (mode === 0) { //关键字搜寻 if (recomment.includes(rule)) { flag = true; txt = rule.substring(0, 8); break; } } } if (flag) {//需要消赞 const raw = name.innerText; name.innerText = `${raw}【❌ 命中规则 ${txt}】`; const succ1 = await changeVote(id, true, sessionID); const succ2 = await changeVote(id, false, sessionID); if (succ1 && succ2) { name.innerText = `${raw}【💔 消赞成功 ${txt}】`; } else { name.innerText = `${raw}【💥 消赞失败(请检查社区是否登陆)】`; } } else { name.innerText += "【💚 无需消赞】"; } row.className += " ru_opt"; } } // 获取SessionID function fetchSessionID() { return new Promise((resolve, reject) => { $http.getText("https://steamcommunity.com/id/Chr_/") .then((text) => { const sid = (text.match(/g_sessionID = "(.+)";/) ?? ["", ""])[1]; resolve([sid !== "", sid]); }).catch((err) => { console.error(err); resolve([false, ""]); }); }); } // 获取评测详情 // 返回 (状态, 评测内容, id , rate) function fetchRecommended(url) { return new Promise((resolve, reject) => { $http.getText(url) .then((text) => { const area = document.createElement("div"); hideArea.appendChild(area); area.innerHTML = text; const recomment = area.querySelector("#ReviewText")?.innerText.trim() ?? "获取失败"; const eleVoteUp = area.querySelector("span[id^='RecommendationVoteUpBtn']"); const voteUp = eleVoteUp?.className.includes("btn_active"); const voteDown = area.querySelector("span[id^='RecommendationVoteDownBtn']")?.className.includes("btn_active"); const voteTag = area.querySelector("span[id^='RecommendationVoteTagBtn']")?.className.includes("btn_active"); const recommentID = eleVoteUp ? parseInt(eleVoteUp.id.replace("RecommendationVoteUpBtn", "")) : 0; // 好评=1 差评=2 欢乐=3 未评价=0 解析失败=-1 const rate = voteUp ? 1 : voteDown ? 2 : voteTag ? 3 : (voteUp == null || voteDown == null || voteTag == null) ? -1 : 0; hideArea.removeChild(area); resolve([true, recomment, recommentID, rate]); }).catch((err) => { console.error(err); resolve([false, "未知错误", 0, 0]); }); }); } // 进行消赞 function changeVote(recID, state, sessionid) { return new Promise((resolve, reject) => { let data = `tagid=1&rateup=${state}&sessionid=${sessionid}`; $http.post(`https://steamcommunity.com/userreviews/votetag/${recID}`, data, { headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, }) .then((json) => { const { success } = json; resolve(success === 1); }).catch((err) => { console.error(err); resolve(false); }); }); } // 通配符匹配 function isMatch(string, pattern) { let dp = []; for (let i = 0; i <= string.length; i++) { let child = []; for (let j = 0; j <= pattern.length; j++) { child.push(false); } dp.push(child); } dp[string.length][pattern.length] = true; for (let i = pattern.length - 1; i >= 0; i--) { if (pattern[i] != "*") { break; } else { dp[string.length][i] = true; } } for (let i = string.length - 1; i >= 0; i--) { for (let j = pattern.length - 1; j >= 0; j--) { if (string[i] == pattern[j] || pattern[j] == "?") { dp[i][j] = dp[i + 1][j + 1]; } else if (pattern[j] == "*") { dp[i][j] = dp[i + 1][j] || dp[i][j + 1]; } else { dp[i][j] = false; } } } return dp[0][0]; }; class Request { 'use strict'; constructor(timeout = 3000) { this.timeout = timeout; } get(url, opt = {}) { return this.#baseRequest(url, 'GET', opt, 'json'); } getText(url, opt = {}) { return this.#baseRequest(url, 'GET', opt, 'text'); } post(url, data, opt = {}) { opt.data = data; return this.#baseRequest(url, 'POST', opt, 'json'); } #baseRequest(url, method = 'GET', opt = {}, responseType = 'json') { Object.assign(opt, { url, method, responseType, timeout: this.timeout }); return new Promise((resolve, reject) => { opt.ontimeout = opt.onerror = reject; opt.onload = ({ readyState, status, response, responseXML, responseText }) => { if (readyState === 4 && status === 200) { if (responseType === 'json') { resolve(response); } else if (responseType === 'text') { resolve(responseText); } else { resolve(responseXML); } } else { console.error('网络错误'); console.log(readyState); console.log(status); console.log(response); reject('解析出错'); } }; GM_xmlhttpRequest(opt); }); } } const $http = new Request(); GM_addStyle(` .feature_banner { background-size: cover; } .feature_banner > div{ margin-left: 10px; color: #fff; font-weight: 200; } .ru_btn { margin-left: 5px; padding: 2px; } .ru_filter { resize: vertical; width: calc(100% - 30px); min-height: 80px; margin: 10px; } `); })();