From 4afa6240bfec7690d5bfc0c5192c29a14aee7f88 Mon Sep 17 00:00:00 2001 From: Fog3211 <23151576+Fog3211@users.noreply.github.com> Date: Wed, 18 Sep 2024 23:20:24 +0800 Subject: [PATCH] refactor: external-link-resolver --- src/@types/tampermonkey.d.ts | 25 +++++++ src/convert-link-to-safty.ts | 81 ---------------------- src/external-link-resolver.ts | 127 ++++++++++++++++++++++++++++++++++ tsconfig.json | 10 ++- 4 files changed, 156 insertions(+), 87 deletions(-) create mode 100644 src/@types/tampermonkey.d.ts delete mode 100644 src/convert-link-to-safty.ts create mode 100644 src/external-link-resolver.ts diff --git a/src/@types/tampermonkey.d.ts b/src/@types/tampermonkey.d.ts new file mode 100644 index 0000000..292601f --- /dev/null +++ b/src/@types/tampermonkey.d.ts @@ -0,0 +1,25 @@ +declare namespace TampermonkeyTypes { + interface GMXMLHttpRequestDetails { + method?: "GET" | "POST" | "HEAD"; + url: string; + headers?: { [key: string]: string }; + data?: string; + onload?: (response: GMXMLHttpRequestResponse) => void; + onerror?: (response: GMXMLHttpRequestResponse) => void; + onabort?: (response: GMXMLHttpRequestResponse) => void; + ontimeout?: (response: GMXMLHttpRequestResponse) => void; + } + + interface GMXMLHttpRequestResponse { + responseText: string; + readyState: number; + status: number; + statusText: string; + responseHeaders: string; + finalUrl: string; + } + + function GM_xmlhttpRequest(details: GMXMLHttpRequestDetails): void; +} + +declare function GM_xmlhttpRequest(details: TampermonkeyTypes.GMXMLHttpRequestDetails): void; \ No newline at end of file diff --git a/src/convert-link-to-safty.ts b/src/convert-link-to-safty.ts deleted file mode 100644 index 396cda9..0000000 --- a/src/convert-link-to-safty.ts +++ /dev/null @@ -1,81 +0,0 @@ -// ==UserScript== -// @name 去除简书、知乎、掘金外链安全限制 -// @namespace https://raw.githubusercontent.com/Fog3211/tampermonkey/gh-pages/convert-link-to-safty.js -// @version 0.2.1 -// @description 去除简书、知乎、掘金外链安全限制,将a标签改为直接跳转 -// @author Fog3211 -// @match https://*.jianshu.com/* -// @match https://*.zhihu.com/* -// @match https://*.juejin.cn/* -// @match https://*.sspai.com/* -// @match https://*.csdn.net/* -// @grant none -// @license MIT -// ==/UserScript== - -(function () { - 'use strict'; - - let loading = false - - const ConfigList = [ - // https://www.jianshu.com/p/c5e07343515d - { key: 'jianshu', linkSelector: ['//link.jianshu.com', '//links.jianshu.com'], splitFlag: 'to=' }, - { key: 'zhihu', linkSelector: ['//link.zhihu.com'], splitFlag: 'target=' }, - { key: 'juejin', linkSelector: ['//link.juejin.cn'], splitFlag: 'target=' }, - { key: 'sspai', linkSelector: ['https://sspai.com'], splitFlag: 'target=' }, - { key: 'csdn', linkSelector: ['https://blog.csdn.net'], splitFlag: 'target=', searchKey: 'target' } - ] - - const getSearchParams = (url: string, key: string) => { - if (!url) return null - const searchParams = new URLSearchParams(url) - return searchParams.get(key) - } - - const rewriteHref = () => { - if (loading) { return } - loading = true - const record = ConfigList.find(u => window.location.hostname.includes(u.key)) - - if (record) { - - const aLists = Array.from( - document.querySelectorAll( - record.linkSelector!.map(u => `a[href*='${u}']`).join(',') - ) - ) as HTMLLinkElement[] - - aLists.forEach(elm => { - /** - * 有可能会出现这种情况,所以要取最后一部分 - * https://link.juejin.cn/?target=https%3A%2F%2Flink.juejin.cn%2F%3Ftarget%3Dhttps%253A%252F%252Fwww.npmjs.com%252Fpackage%252Fevents - */ - const matchs = decodeURIComponent(elm.href).split(record.splitFlag!) - if (matchs.length === 2) { - elm.setAttribute('href', matchs.pop() as string) - } else if (matchs.length > 2) { - elm.setAttribute('href', decodeURIComponent(matchs.pop() as string)) - } - }) - } - loading = false - } - - const redirectUrl = () => { - const record = ConfigList.find(u => window.location.hostname.includes(u.key)) - if (record?.searchKey) { - const targetUrl = getSearchParams(window.location.href.split('?')[1], record.searchKey) - if (targetUrl) { - window.location.href = targetUrl - } - } - } - - redirectUrl() - - rewriteHref(); - setInterval(() => { - rewriteHref(); - }, 3000); -})(); diff --git a/src/external-link-resolver.ts b/src/external-link-resolver.ts new file mode 100644 index 0000000..b320487 --- /dev/null +++ b/src/external-link-resolver.ts @@ -0,0 +1,127 @@ +// ==UserScript== +// @name Universal External Link Direct Redirect +// @namespace http://tampermonkey.net/ +// @version 1.0.0 +// @description Process external links on various websites to enable direct redirection to target sites +// @match https://juejin.cn/* +// @match https://link.juejin.cn/* +// @match https://segmentfault.com/* +// @match https://link.segmentfault.com/* +// @grant GM_xmlhttpRequest +// ==/UserScript== + +interface SiteConfig { + directMatch: string; + linkSelector: string; + extractTarget: (url: URL) => Promise | string | null; +} + +interface SiteConfigs { + [key: string]: SiteConfig; +} + +(function () { + "use strict"; + + const siteConfigs: SiteConfigs = { + "juejin": { + directMatch: "link.juejin.cn", + linkSelector: "a[href^=\"https://link.juejin.cn\"]", + extractTarget: async (url: URL): Promise => { + const params = new URLSearchParams(url.search); + const target = params.get("target"); + if (target) { + if (target.startsWith("https://link.juejin.cn")) { + return siteConfigs["juejin"].extractTarget(new URL(target)); + } + return decodeURIComponent(target); + } + return null; + } + }, + "segmentfault": { + directMatch: "link.segmentfault.com", + linkSelector: "a[href^=\"https://link.segmentfault.com\"]", + extractTarget: async (url: URL): Promise => { + // TODO: Optimize SegmentFault link processing logic, consider more efficient methods + return new Promise((resolve) => { + GM_xmlhttpRequest({ + method: "GET", + url: url.href, + headers: { + "Referer": "https://segmentfault.com", + }, + onload: function (response: TampermonkeyTypes.GMXMLHttpRequestResponse) { + const parser = new DOMParser(); + const doc = parser.parseFromString(response.responseText, "text/html"); + const dataUrl = doc.body.getAttribute("data-url"); + resolve(dataUrl || url.href); + }, + onerror: function () { + resolve(url.href); + } + }); + }); + } + }, + "csdn": { + directMatch: "link.csdn.net", + linkSelector: "a[href^=\"https://link.csdn.net/\"]", + extractTarget: (url: URL): string | null => { + const params = new URLSearchParams(url.search); + return params.get("target"); + } + } + }; + + async function processRule(config: SiteConfig): Promise { + if (window.location.hostname === config.directMatch) { + let targetUrl = await config.extractTarget(new URL(window.location.href)); + while (targetUrl && targetUrl.startsWith("https://link.juejin.cn")) { + // Keep resolving until we get the final non-Juejin link + targetUrl = await siteConfigs["juejin"].extractTarget(new URL(targetUrl)); + } + if (targetUrl) { + window.location.href = targetUrl; + } + return; + } + + await replaceLinks(config); + } + + async function replaceLinks(config: SiteConfig): Promise { + const links = document.querySelectorAll(config.linkSelector); + for (const link of links) { + const href = link.getAttribute("href"); + if (href) { + const targetUrl = await config.extractTarget(new URL(href)); + if (targetUrl) { + link.href = decodeURIComponent(targetUrl); + link.target = "_blank"; + link.rel = "noopener noreferrer"; + } + } + } + } + + function observeDOMChanges(config: SiteConfig): void { + const observer = new MutationObserver(() => replaceLinks(config)); + observer.observe(document.body, { childList: true, subtree: true }); + } + + async function init(): Promise { + const currentSite = Object.keys(siteConfigs).find(site => + window.location.hostname.includes(site) || + window.location.hostname.includes(siteConfigs[site].directMatch) + ); + + if (currentSite) { + const config = siteConfigs[currentSite]; + await processRule(config); + observeDOMChanges(config); + } + } + + init(); +})(); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index c5f75bc..8210085 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "strict": true, "module": "commonjs", "skipLibCheck": true, - "target": "es5", + "target": "esnext", "outDir": "dist", "baseUrl": "./", "paths": { @@ -21,13 +21,11 @@ "lib": [ "dom", "dom.iterable", - "es2015", "esnext" - ] + ], + "typeRoots": ["./src/@types", "./node_modules/@types"] }, - "include": [ - "src" - ], + "include": ["src/**/*"], "exclude": [ "node_modules", "build",