import { defaultRules } from "./defaultRules"; import { addPreSendListener, addPreEditListener, MessageObject, removePreSendListener, removePreEditListener, } from "../../api/MessageEvents"; import definePlugin from "../../utils/types"; // From lodash const reRegExpChar = /[\\^$.*+?()[\]{}|]/g; const reHasRegExpChar = RegExp(reRegExpChar.source); export default definePlugin({ name: "clearURLs", description: "Removes tracking garbage from URLs", authors: [ { name: "adryd", id: 0n, }, ], 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); }, });