NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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 // @updateURL https://github.com/VChet/telegram-ad-filter/raw/master/tg-ad-filter.user.js // @downloadURL https://github.com/VChet/telegram-ad-filter/raw/master/tg-ad-filter.user.js // ==/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"] }); })();