Recommend_Unrate

批量撤回评测点赞/有趣

Szkript telepítése?
A szerző által javasolt szkript

Ez is érdekelhet: Show_English_Name

Szkript telepítése

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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