Vencord/src/plugins/clearURLs/index.ts

150 lines
4.7 KiB
TypeScript

/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {
addPreEditListener,
addPreSendListener,
MessageObject,
removePreEditListener,
removePreSendListener
} from "@api/MessageEvents";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { defaultRules } from "./defaultRules";
// From lodash
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
const reHasRegExpChar = RegExp(reRegExpChar.source);
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);
},
});