/* * Vencord, a modification for Discord's desktop app * Copyright (c) 2022 Vendicated and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import { addPreEditListener, addPreSendListener, MessageObject, removePreEditListener, removePreSendListener } from "@api/MessageEvents"; import { migratePluginSettings } from "@api/settings"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { defaultRules } from "./defaultRules"; // From lodash const reRegExpChar = /[\\^$.*+?()[\]{}|]/g; const reHasRegExpChar = RegExp(reRegExpChar.source); migratePluginSettings("ClearURLs", "clearURLs"); export default definePlugin({ name: "ClearURLs", description: "Removes tracking garbage from URLs", authors: [Devs.adryd], dependencies: ["MessageEventsAPI"], escapeRegExp(str: string) { return (str && reHasRegExpChar.test(str)) ? str.replace(reRegExpChar, "\\$&") : (str || ""); }, createRules() { // Can be extended upon once user configs are available // Eg. (useDefaultRules: boolean, customRules: Array[string]) const rules = defaultRules; this.universalRules = new Set(); this.rulesByHost = new Map(); this.hostRules = new Map(); for (const rule of rules) { const splitRule = rule.split("@"); const paramRule = new RegExp( "^" + this.escapeRegExp(splitRule[0]).replace(/\\\*/, ".+?") + "$" ); if (!splitRule[1]) { this.universalRules.add(paramRule); continue; } const hostRule = new RegExp( "^(www\\.)?" + this.escapeRegExp(splitRule[1]) .replace(/\\\./, "\\.") .replace(/^\\\*\\\./, "(.+?\\.)?") .replace(/\\\*/, ".+?") + "$" ); const hostRuleIndex = hostRule.toString(); this.hostRules.set(hostRuleIndex, hostRule); if (this.rulesByHost.get(hostRuleIndex) == null) { this.rulesByHost.set(hostRuleIndex, new Set()); } this.rulesByHost.get(hostRuleIndex).add(paramRule); } }, removeParam(rule: string | RegExp, param: string, parent: URLSearchParams) { if (param === rule || rule instanceof RegExp && rule.test(param)) { parent.delete(param); } }, replacer(match: string) { // Parse URL without throwing errors try { var url = new URL(match); } catch (error) { // Don't modify anything if we can't parse the URL return match; } // Cheap way to check if there are any search params if (url.searchParams.entries().next().done) { // If there are none, we don't need to modify anything return match; } // Check all universal rules this.universalRules.forEach(rule => { url.searchParams.forEach((_value, param, parent) => { this.removeParam(rule, param, parent); }); }); // Check rules for each hosts that match this.hostRules.forEach((regex, hostRuleName) => { if (!regex.test(url.hostname)) return; this.rulesByHost.get(hostRuleName).forEach(rule => { url.searchParams.forEach((_value, param, parent) => { this.removeParam(rule, param, parent); }); }); }); return url.toString(); }, onSend(msg: MessageObject) { // Only run on messages that contain URLs if (msg.content.match(/http(s)?:\/\//)) { msg.content = msg.content.replace( /(https?:\/\/[^\s<]+[^<.,:;"'>)|\]\s])/g, match => this.replacer(match) ); } }, start() { this.createRules(); this.preSend = addPreSendListener((_, msg) => this.onSend(msg)); this.preEdit = addPreEditListener((_cid, _mid, msg) => this.onSend(msg) ); }, stop() { removePreSendListener(this.preSend); removePreEditListener(this.preEdit); }, });