Recommend_Unrate

批量撤回评测点赞/有趣

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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;
      }     
      `);
})();