Greasy Fork

Greasy Fork is available in English.

批量撤回评测点赞

批量撤回评测点赞/有趣

当前为 2022-03-05 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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.0
// @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 = [
        "pan",
        "share",
        "weiyun",
        "lanzou",
        "baidu",
    ].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. Steam 评测是社区的重要组成部分, 请尽量使用黑名单进行消赞</p>
    <p> 4. 一些常用的规则参见 【<a href="#" target="_blank">发布帖</a>】</p>
    <p> 5. 如果需要使用正则表达式, 请以 $$ 开头</p>
    <p> 6. 如果需要对所有评测消赞, 请填入 * </p>`;
    banner.appendChild(describe);
    const filter = document.createElement('textarea');
    filter.placeholder = "黑名单规则, 一行一条, 支持 * ? 作为通配符";
    filter.className = "ru_filter";
    filter.value = window.localStorage.getItem("ru_rules") ?? 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(rateTable);
            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("$$")) {
                    try {
                        return [2, new RegExp(x.slice(2), "ig")];
                    } catch (e) {
                        ShowDialog("正则表达式有误", x);
                        return [-1, null];
                    }
                }
                else if (x.includes("*") || x.includes("?")) {
                    return [1, x];
                }
                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;
            for (const [mode, rule] of rules) {
                if (mode === 2) {// 正则模式
                    if (recomment.search(rule) !== -1) {
                        flag = true;
                        break;
                    }
                } else if (mode === 1) {//简易通配符
                    if (isMatch(recomment, rule)) {
                        flag = true;
                        break;
                    }
                } else if (mode === 0) { //关键字搜寻
                    if (recomment.includes(rule)) {
                        flag = true;
                        break;
                    }
                }
            }
            if (flag) {//需要消赞
                const raw = name.innerText;
                name.innerText = `${raw}【❌ 命中规则】`;
                const succ1 = await changeVote(id, true, sessionID);
                const succ2 = await changeVote(id, false, sessionID);

                if (succ1 && succ2) {
                    name.innerText = `${raw}【💔 消赞成功】`;
                } 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, "未知错误 :" + err, 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;
      }     
      `);
})();