WeChat Plus

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

  1. // ==UserScript==
  2. // @name WeChat Plus
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1.0
  5. // @description 针对微信公众号文章的增强脚本
  6. // @author PRO-2684
  7. // @match https://mp.weixin.qq.com/s/*
  8. // @run-at document-start
  9. // @icon https://res.wx.qq.com/a/wx_fed/assets/res/MjliNWVm.svg
  10. // @license gpl-3.0
  11. // @grant unsafeWindow
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // @grant GM_deleteValue
  15. // @grant GM_registerMenuCommand
  16. // @grant GM_unregisterMenuCommand
  17. // @grant GM_addValueChangeListener
  18. // @require https://github.com/PRO-2684/GM_config/releases/download/v1.2.1/config.min.js#md5=525526b8f0b6b8606cedf08c651163c2
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. 'use strict';
  23. const { name, version } = GM_info.script;
  24. const idPrefix = "wechat-plus-";
  25. const $ = document.querySelector.bind(document);
  26. const debug = console.debug.bind(console, `[${name}@${version}]`);
  27. const error = console.error.bind(console, `[${name}@${version}]`);
  28. const configDesc = {
  29. $default: {
  30. autoClose: false,
  31. },
  32. viewCover: {
  33. name: "🖼️ 查看封面",
  34. title: "在新标签页中打开封面",
  35. type: "action",
  36. },
  37. showSummary: {
  38. name: "📄 显示摘要",
  39. type: "bool",
  40. },
  41. allowCopy: {
  42. name: "📋 允许复制",
  43. title: "允许复制所有内容",
  44. type: "bool",
  45. },
  46. hideBottomBar: {
  47. name: "⬇️ 隐藏底栏",
  48. title: "隐藏毫无作用的底栏",
  49. type: "bool",
  50. },
  51. blockReport: {
  52. name: "🚫 屏蔽上报*",
  53. title: "屏蔽信息上报,避免隐私泄露,需要刷新页面生效",
  54. type: "bool",
  55. },
  56. };
  57. const config = new GM_config(configDesc);
  58.  
  59. // Helper functions
  60. /**
  61. * Resolves when the document is ready.
  62. */
  63. async function onReady() {
  64. return new Promise((resolve) => {
  65. if (document.readyState === "complete") {
  66. resolve();
  67. } else {
  68. document.addEventListener("DOMContentLoaded", () => {
  69. resolve();
  70. }, { once: true });
  71. }
  72. });
  73. }
  74. /**
  75. * Toggles the given style on or off.
  76. */
  77. function toggleStyle(id, toggle) {
  78. const existing = document.getElementById(idPrefix + id);
  79. if (existing && !toggle) {
  80. existing.remove();
  81. } else if (!existing && toggle) {
  82. const styleElement = document.createElement("style");
  83. styleElement.id = idPrefix + id;
  84. styleElement.textContent = styles[id];
  85. document.head.appendChild(styleElement);
  86. }
  87. }
  88.  
  89. // Main functions
  90. function viewCover() {
  91. const meta = $("meta[property='og:image']");
  92. const url = meta?.content;
  93. if (url) {
  94. window.open(url, "_blank");
  95. } else {
  96. alert("Cannot find cover image URL.");
  97. }
  98. }
  99. function showSummary(show) {
  100. const block = $("#meta_content");
  101. if (!block) {
  102. error("Cannot find meta content block.");
  103. return;
  104. }
  105. const summary = block.querySelector("#summary");
  106. if (summary && !show) {
  107. summary.remove();
  108. } else if (!summary && show) {
  109. const meta = $("meta[name='description']");
  110. const description = meta?.content;
  111. if (!description) {
  112. error("Cannot find summary description.");
  113. return;
  114. }
  115. const summary = document.createElement("span");
  116. summary.id = "summary";
  117. summary.style.display = "block";
  118. summary.style.borderLeft = "0.2em solid";
  119. summary.style.paddingLeft = "0.5em";
  120. summary.classList.add("rich_media_meta", "rich_media_meta_text");
  121. summary.textContent = description;
  122. block.appendChild(summary);
  123. }
  124. }
  125. function allowCopy(allow) {
  126. const body = document.body;
  127. body.classList.toggle("use-femenu", !allow);
  128. }
  129. const hideBottomBar = "#unlogin_bottom_bar { display: none !important; }" +
  130. "body#activity-detail { padding-bottom: 0 !important; }";
  131. function blockReport() {
  132. function shouldBlock(url) {
  133. const blockList = new Set([
  134. // Additional info, like albums, etc.
  135. // "mp.weixin.qq.com/mp/getappmsgext",
  136.  
  137. // CSP report, can't be blocked by UserScript - Use [uBlock Origin](https://github.com/gorhill/uBlock) to block it
  138. // "mp.weixin.qq.com/mp/fereport",
  139.  
  140. // Will return error anyway (errmsg: "no session")
  141. "mp.weixin.qq.com/mp/appmsg_comment",
  142. "mp.weixin.qq.com/mp/relatedsearchword",
  143. "mp.weixin.qq.com/mp/getbizbanner",
  144. // Information collection
  145. "mp.weixin.qq.com/mp/getappmsgad",
  146. "mp.weixin.qq.com/mp/jsmonitor",
  147. "mp.weixin.qq.com/mp/wapcommreport",
  148. "mp.weixin.qq.com/mp/appmsgreport",
  149. "badjs.weixinbridge.com/badjs",
  150. "badjs.weixinbridge.com/report",
  151. "open.weixin.qq.com/pcopensdk/report",
  152. ]);
  153. url = new URL(url, location.href);
  154. const identifier = url.hostname + url.pathname;
  155. return blockList.has(identifier);
  156. }
  157. // Overwrite `XMLHttpRequest`
  158. const originalOpen = unsafeWindow.XMLHttpRequest.prototype.open;
  159. unsafeWindow.XMLHttpRequest.prototype.open = function (...args) {
  160. const url = args[1];
  161. if (shouldBlock(url)) {
  162. debug("Blocked opening:", url);
  163. this._url = url;
  164. } else {
  165. return originalOpen.apply(this, args);
  166. }
  167. }
  168. const originalSet = unsafeWindow.XMLHttpRequest.prototype.setRequestHeader;
  169. unsafeWindow.XMLHttpRequest.prototype.setRequestHeader = function (...args) {
  170. if (this._url) {
  171. debug("Blocked setting header:", this._url, ...args);
  172. } else {
  173. return originalSet.apply(this, args);
  174. }
  175. }
  176. const originalSend = unsafeWindow.XMLHttpRequest.prototype.send;
  177. unsafeWindow.XMLHttpRequest.prototype.send = function (...args) {
  178. if (this._url) {
  179. debug("Blocked sending:", this._url, ...args);
  180. } else {
  181. return originalSend.apply(this, args);
  182. }
  183. }
  184. // Filter setting `src` of images
  185. const { get, set } = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, "src");
  186. Object.defineProperty(HTMLImageElement.prototype, "src", {
  187. get() {
  188. return get.call(this);
  189. },
  190. set(url) {
  191. if (shouldBlock(url)) {
  192. debug("Blocked image url:", url);
  193. return url;
  194. } else {
  195. return set.call(this, url);
  196. }
  197. },
  198. });
  199. }
  200.  
  201. // Once: Functions that are called once when the script is loaded.
  202. if (config.get("blockReport")) {
  203. blockReport();
  204. }
  205.  
  206. // Actions: Functions that are called when the user clicks on it.
  207. const actions = {
  208. viewCover,
  209. };
  210. config.addEventListener("get", (e) => {
  211. const action = actions[e.detail.prop];
  212. if (action) {
  213. action();
  214. }
  215. });
  216.  
  217. // Callbacks: Functions that are called when the config is changed.
  218. const callbacks = {
  219. showSummary,
  220. allowCopy,
  221. };
  222. onReady().then(() => {
  223. for (const [prop, callback] of Object.entries(callbacks)) {
  224. callback(config.get(prop));
  225. }
  226. });
  227.  
  228. // Styles: CSS styles that can be toggled on and off.
  229. const styles = {
  230. hideBottomBar,
  231. };
  232. for (const prop of Object.keys(styles)) {
  233. toggleStyle(prop, config.get(prop));
  234. }
  235.  
  236. config.addEventListener("set", (e) => {
  237. const callback = callbacks[e.detail.prop];
  238. if (callback) {
  239. onReady().then(() => {
  240. callback(e.detail.after);
  241. });
  242. }
  243. if (e.detail.prop in styles) {
  244. toggleStyle(e.detail.prop, e.detail.after);
  245. }
  246. });
  247. })();
  248.