Recommend_Unrate

批量撤回评测点赞/有趣

Instalar este script¿?
Script recomendado por el autor

Puede que también te guste Show_English_Name.

Instalar este script
// ==UserScript==
// @name:zh-CN      批量撤回评测点赞
// @name            Recommend_Unrate
// @namespace       https://blog.chrxw.com
// @supportURL      https://blog.chrxw.com/scripts.html
// @contributionURL https://afdian.net/@chr233
// @version         1.11
// @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;
      }     
      `);
})();