WeChat Plus

针对微信公众号文章的增强脚本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         WeChat Plus
// @namespace    http://tampermonkey.net/
// @version      0.1.1
// @description  针对微信公众号文章的增强脚本
// @author       PRO-2684
// @match        https://mp.weixin.qq.com/s/*
// @run-at       document-start
// @icon         https://res.wx.qq.com/a/wx_fed/assets/res/MjliNWVm.svg
// @license      gpl-3.0
// @grant        unsafeWindow
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_addValueChangeListener
// @require      https://github.com/PRO-2684/GM_config/releases/download/v1.2.2/config.min.js#md5=c45f9b0d19ba69bb2d44918746c4d7ae
// ==/UserScript==

(function () {
    'use strict';
    const { name, version } = GM_info.script;
    const idPrefix = "wechat-plus-";
    const $ = document.querySelector.bind(document);
    const debug = console.debug.bind(console, `[${name}@${version}]`);
    const error = console.error.bind(console, `[${name}@${version}]`);
    const configDesc = {
        $default: {
            autoClose: false,
        },
        viewCover: {
            name: "🖼️ 查看封面",
            title: "在新标签页中打开封面",
            type: "action",
        },
        showSummary: {
            name: "📄 显示摘要",
            type: "bool",
        },
        allowCopy: {
            name: "📋 允许复制",
            title: "允许复制所有内容",
            type: "bool",
        },
        hideBottomBar: {
            name: "⬇️ 隐藏底栏",
            title: "隐藏毫无作用的底栏",
            type: "bool",
        },
        blockReport: {
            name: "🚫 屏蔽上报*",
            title: "屏蔽信息上报,避免隐私泄露,需要刷新页面生效",
            type: "bool",
        },
    };
    const config = new GM_config(configDesc);

    // Helper functions
    /**
     * Resolves when the document is ready.
     */
    async function onReady() {
        return new Promise((resolve) => {
            if (document.readyState === "complete") {
                resolve();
            } else {
                document.addEventListener("DOMContentLoaded", () => {
                    resolve();
                }, { once: true });
            }
        });
    }
    /**
     * Toggles the given style on or off.
     */
    function toggleStyle(id, toggle) {
        const existing = document.getElementById(idPrefix + id);
        if (existing && !toggle) {
            existing.remove();
        } else if (!existing && toggle) {
            const styleElement = document.createElement("style");
            styleElement.id = idPrefix + id;
            styleElement.textContent = styles[id];
            document.head.appendChild(styleElement);
        }
    }

    // Main functions
    function viewCover() {
        const meta = $("meta[property='og:image']");
        const url = meta?.content;
        if (url) {
            window.open(url, "_blank");
        } else {
            alert("Cannot find cover image URL.");
        }
    }
    function showSummary(show) {
        const block = $("#meta_content");
        if (!block) {
            error("Cannot find meta content block.");
            return;
        }
        const summary = block.querySelector("#summary");
        if (summary && !show) {
            summary.remove();
        } else if (!summary && show) {
            const meta = $("meta[name='description']");
            const description = meta?.content;
            if (!description) {
                error("Cannot find summary description.");
                return;
            }
            const summary = document.createElement("span");
            summary.id = "summary";
            summary.style.display = "block";
            summary.style.borderLeft = "0.2em solid";
            summary.style.paddingLeft = "0.5em";
            summary.classList.add("rich_media_meta", "rich_media_meta_text");
            summary.textContent = description;
            block.appendChild(summary);
        }
    }
    function allowCopy(allow) {
        const body = document.body;
        body.classList.toggle("use-femenu", !allow);
    }
    const hideBottomBar = "#unlogin_bottom_bar { display: none !important; }" +
        "body#activity-detail { padding-bottom: 0 !important; }";
    function blockReport() {
        function shouldBlock(url) {
            const blockList = new Set([
                // Additional info, like albums, etc.
                // "mp.weixin.qq.com/mp/getappmsgext",

                // CSP report, can't be blocked by UserScript - Use [uBlock Origin](https://github.com/gorhill/uBlock) to block it
                // "mp.weixin.qq.com/mp/fereport",

                // Will return error anyway (errmsg: "no session")
                "mp.weixin.qq.com/mp/appmsg_comment",
                "mp.weixin.qq.com/mp/relatedsearchword",
                "mp.weixin.qq.com/mp/getbizbanner",
                // Information collection
                "mp.weixin.qq.com/mp/getappmsgad",
                "mp.weixin.qq.com/mp/jsmonitor",
                "mp.weixin.qq.com/mp/wapcommreport",
                "mp.weixin.qq.com/mp/appmsgreport",
                "badjs.weixinbridge.com/badjs",
                "badjs.weixinbridge.com/report",
                "open.weixin.qq.com/pcopensdk/report",
            ]);
            url = new URL(url, location.href);
            const identifier = url.hostname + url.pathname;
            return blockList.has(identifier);
        }
        // Overwrite `XMLHttpRequest`
        const originalOpen = unsafeWindow.XMLHttpRequest.prototype.open;
        unsafeWindow.XMLHttpRequest.prototype.open = function (...args) {
            const url = args[1];
            if (shouldBlock(url)) {
                debug("Blocked opening:", url);
                this._url = url;
            } else {
                return originalOpen.apply(this, args);
            }
        }
        const originalSet = unsafeWindow.XMLHttpRequest.prototype.setRequestHeader;
        unsafeWindow.XMLHttpRequest.prototype.setRequestHeader = function (...args) {
            if (this._url) {
                debug("Blocked setting header:", this._url, ...args);
            } else {
                return originalSet.apply(this, args);
            }
        }
        const originalSend = unsafeWindow.XMLHttpRequest.prototype.send;
        unsafeWindow.XMLHttpRequest.prototype.send = function (...args) {
            if (this._url) {
                debug("Blocked sending:", this._url, ...args);
            } else {
                return originalSend.apply(this, args);
            }
        }
        // Filter setting `src` of images
        const { get, set } = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, "src");
        Object.defineProperty(HTMLImageElement.prototype, "src", {
            get() {
                return get.call(this);
            },
            set(url) {
                if (shouldBlock(url)) {
                    debug("Blocked image url:", url);
                    return url;
                } else {
                    return set.call(this, url);
                }
            },
        });
    }

    // Once: Functions that are called once when the script is loaded.
    if (config.get("blockReport")) {
        blockReport();
    }

    // Actions: Functions that are called when the user clicks on it.
    const actions = {
        viewCover,
    };
    config.addEventListener("get", (e) => {
        const action = actions[e.detail.prop];
        if (action) {
            action();
        }
    });

    // Callbacks: Functions that are called when the config is changed.
    const callbacks = {
        showSummary,
        allowCopy,
    };
    onReady().then(() => {
        for (const [prop, callback] of Object.entries(callbacks)) {
            callback(config.get(prop));
        }
    });

    // Styles: CSS styles that can be toggled on and off.
    const styles = {
        hideBottomBar,
    };
    for (const prop of Object.keys(styles)) {
        toggleStyle(prop, config.get(prop));
    }

    config.addEventListener("set", (e) => {
        const callback = callbacks[e.detail.prop];
        if (callback) {
            onReady().then(() => {
                callback(e.detail.after);
            });
        }
        if (e.detail.prop in styles) {
            toggleStyle(e.detail.prop, e.detail.after);
        }
    });
})();