Collapses messages that contain words from the ad-word list
// ==UserScript==
// @name Telegram Ad Filter
// @version 1.4.1
// @description Collapses messages that contain words from the ad-word list
// @license MIT
// @author VChet
// @icon https://web.telegram.org/favicon.ico
// @namespace telegram-ad-filter
// @match https://web.telegram.org/k/*
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @homepage https://github.com/VChet/telegram-ad-filter
// @homepageURL https://github.com/VChet/telegram-ad-filter
// @supportURL https://github.com/VChet/telegram-ad-filter
// ==/UserScript==
/* jshint esversion: 11 */
// src/DOM.ts
var globalStyles = `
.bubble:not(.has-advertisement) .advertisement,
.bubble.has-advertisement .bubble-content *:not(.advertisement),
.bubble.has-advertisement .reply-markup {
display: none;
}
.advertisement {
padding: 0.5rem 1rem;
cursor: pointer;
white-space: nowrap;
font-style: italic;
font-size: var(--messages-text-size);
font-weight: var(--font-weight-bold);
color: var(--link-color);
}
#telegram-ad-filter-settings {
display: inline-flex;
justify-content: center;
width: 24px;
font-size: 24px;
color: transparent;
text-shadow: 0 0 var(--secondary-text-color);
}
`;
var frameStyle = `
inset: 115px auto auto 130px;
border: 1px solid rgb(0, 0, 0);
height: 300px;
margin: 0px;
max-height: 95%;
max-width: 95%;
opacity: 1;
overflow: auto;
padding: 0px;
position: fixed;
width: 75%;
z-index: 9999;
display: block;
`;
var popupStyle = `
#telegram-ad-filter {
background: #181818;
color: #ffffff;
}
#telegram-ad-filter textarea {
resize: vertical;
width: 100%;
min-height: 150px;
}
#telegram-ad-filter .reset, #telegram-ad-filter .reset a, #telegram-ad-filter_buttons_holder {
color: inherit;
}
`;
function addSettingsButton(element, callback) {
const settingsButton = document.createElement("button");
settingsButton.classList.add("btn-icon", "rp");
settingsButton.setAttribute("title", "Telegram Ad Filter Settings");
const ripple = document.createElement("div");
ripple.classList.add("c-ripple");
const icon = document.createElement("span");
icon.id = "telegram-ad-filter-settings";
icon.textContent = "\u2699\uFE0F";
settingsButton.append(ripple);
settingsButton.append(icon);
settingsButton.addEventListener("click", (event) => {
event.stopPropagation();
callback();
});
element.append(settingsButton);
}
function handleMessageNode(node, adWords) {
const message = node.querySelector(".message");
if (!message || node.querySelector(".advertisement")) {
return;
}
const textContent = message.textContent?.toLowerCase();
const links = [...message.querySelectorAll("a")].reduce((acc, { href }) => {
if (href) {
acc.push(href.toLowerCase());
}
return acc;
}, []);
if (!textContent && !links.length) {
return;
}
const filters = adWords.map((filter) => filter.toLowerCase());
const hasMatch = filters.some(
(filter) => textContent?.includes(filter) || links.some((href) => href.includes(filter))
);
if (!hasMatch) {
return;
}
const trigger = document.createElement("div");
trigger.classList.add("advertisement");
trigger.textContent = "Hidden by filter";
node.querySelector(".bubble-content")?.prepend(trigger);
node.classList.add("has-advertisement");
trigger.addEventListener("click", () => {
node.classList.remove("has-advertisement");
});
message.addEventListener("click", () => {
node.classList.add("has-advertisement");
});
}
// src/configs.ts
var settingsConfig = {
id: "telegram-ad-filter",
frameStyle,
css: popupStyle,
title: "Telegram Ad Filter Settings",
fields: {
listUrls: {
label: "Blacklist URLs (one per line) \u2013 each URL must be a publicly accessible JSON file containing an array of blocked words or phrases",
type: "textarea",
default: "https://raw.githubusercontent.com/VChet/telegram-ad-filter/master/blacklist.json"
}
}
};
// src/fetch.ts
function isValidURL(payload) {
try {
if (typeof payload !== "string") {
return false;
}
const parsedUrl = new URL(payload);
return parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:";
} catch {
return false;
}
}
function isValidJSON(payload) {
try {
JSON.parse(payload);
return true;
} catch {
return false;
}
}
async function fetchAndParseJSON(url) {
const content = await fetch(url).then((response) => response.text());
if (!isValidJSON(content)) {
throw new SyntaxError(`Invalid JSON: data from ${url}`);
}
return JSON.parse(content);
}
async function fetchLists(urlsString) {
const urls = urlsString.split("\n").map((url) => url.trim()).filter(Boolean);
const resultSet = /* @__PURE__ */ new Set();
for (const url of urls) {
if (!isValidURL(url)) {
throw new URIError(`Invalid URL: ${url}. Please ensure it leads to an online source like GitHub, Gist, Pastebin, etc.`);
}
try {
const parsedData = await fetchAndParseJSON(url);
if (!Array.isArray(parsedData)) {
throw new TypeError(`Invalid array: data from ${url}`);
}
const strings = parsedData.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean);
for (const string of strings) {
resultSet.add(string);
}
} catch (error) {
if (error instanceof SyntaxError) {
throw error;
}
throw new Error(`Fetch error: ${url}. Please check the URL or your network connection.`);
}
}
return [...resultSet];
}
// src/main.ts
(async () => {
GM_addStyle(globalStyles);
let adWords = [];
const gmc = new GM_configStruct({
...settingsConfig,
events: {
init: async function() {
adWords = await fetchLists(this.get("listUrls").toString());
},
save: async function() {
try {
adWords = await fetchLists(this.get("listUrls").toString());
this.close();
} catch (error) {
alert(error instanceof Error ? error.message : String(error));
}
}
}
});
function walk(node) {
if (!(node instanceof HTMLElement) || !node.nodeType) {
return;
}
let child = null;
let next = null;
switch (node.nodeType) {
case node.ELEMENT_NODE:
case node.DOCUMENT_NODE:
case node.DOCUMENT_FRAGMENT_NODE:
if (node.matches(".chat-utils")) {
addSettingsButton(node, () => {
gmc.open();
});
}
if (node.matches(".bubble")) {
handleMessageNode(node, adWords);
}
child = node.firstChild;
while (child) {
next = child.nextSibling;
walk(child);
child = next;
}
break;
case node.TEXT_NODE:
default:
break;
}
}
function mutationHandler(mutationRecords) {
for (const { type, addedNodes } of mutationRecords) {
if (type === "childList" && typeof addedNodes === "object" && addedNodes.length) {
for (const node of addedNodes) {
walk(node);
}
}
}
}
const observer = new MutationObserver(mutationHandler);
observer.observe(document, { childList: true, subtree: true, attributeFilter: ["class"] });
})();