ADallower

makes your ads much better

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         ADallower
// @namespace    http://tampermonkey.net/
// @version      2026-01-11
// @description  makes your ads much better
// @author       hackatimefraud
// @match        https://*/*
// @match        http://*/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @run-at       document-start
// @connect      *
// @license      GPLv3
// ==/UserScript==

/* jshint esversion: 11 */

(function () {
    "use strict";

    const ONLY_ADS = true; // true = only replace ADs
    const BACKEND = "https://ads.shymike.dev"; // which url to use for the backend
    const IMAGE_EXT_RE = /\.(png|jpe?g|gif|webp|avif|bmp|svg)(\?.*)?$/i; // regex fallback for image urls
    const FALLBACK_PIXEL =
        "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";

    /**
     * replace iframe ads
     */
    function handleIframe(iframe) {
        if (!iframe || !iframe.src) return;

        if (isAdUrl(iframe.src)) {
            const placeholder = document.createElement("img");
            fetchReplacement().then(({ dataUrl }) => {
                placeholder.src = dataUrl;
            });
            placeholder.style.width = iframe.offsetWidth + "px";
            placeholder.style.height = iframe.offsetHeight + "px";

            iframe.replaceWith(placeholder);
        }
    }

    /**
     * replace background ads in an element
     */
    function replaceBackgroundAds(el) {
        if (document.readyState === "loading") return;
        if (!el || el.nodeType !== 1) return;

        const style = getComputedStyle(el);
        const bg = style.backgroundImage;
        if (!bg || bg === "none") return;

        const match = bg.match(/url\(["']?(.*?)["']?\)/);
        if (!match) return;

        const url = match[1];
        if (!ONLY_ADS || isAdUrl(url)) {
            fetchReplacement().then(({ dataUrl }) => {
                el.style.backgroundImage = `url("${dataUrl}")`;
            });
        }
    }

    /**
     * check if a hostname matches a blocklist rule
     */
    function hostnameMatches(hostname, rule) {
        if (rule.startsWith("*.")) {
            return hostname.endsWith(rule.slice(1));
        }
        return hostname === rule || hostname.endsWith("." + rule);
    }

    /**
     * check if a url is an ad based on the blocklist
     */
    function isAdUrl(url) {
        if (ONLY_ADS && BLOCKLIST.length === 0) return false;

        let parsed;
        try {
            parsed = new URL(url, location.href);
        } catch {
            return false;
        }

        return BLOCKLIST.some((rule) => hostnameMatches(parsed.hostname, rule));
    }

    // check how many images the server has
    let totalImages = 0;
    GM_xmlhttpRequest({
        method: "GET",
        url: `${BACKEND}/count`,
        onload: function (response) {
            totalImages = parseInt(response.responseText, 10) || 0;
        },
    });

    /**
     * convert an ArrayBuffer to a data URL
     */
    function bufferToDataUrl(buffer, contentType) {
        const bytes = new Uint8Array(buffer);
        let binary = "";
        for (let i = 0; i < bytes.length; i++)
            binary += String.fromCharCode(bytes[i]);
        const base64 = btoa(binary);
        const type = contentType || "image/png";
        return `data:${type};base64,${base64}`;
    }

    let cachedImages = {}; // cache each image index's promise

    /**
     * fetch a replament image from the backend
     */
    function fetchReplacement() {
        const index = Math.floor(Math.random() * totalImages);
        if (cachedImages[index]) {
            return cachedImages[index];
        }

        let imageResponse = new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `${BACKEND}/image/${index}`,
                responseType: "arraybuffer",
                onload: (resp) => {
                    const header = resp.responseHeaders || "";
                    const match = header.match(/content-type:\s*([^\n;]+)/i);
                    const contentType = match ? match[1].trim() : "image/png";
                    try {
                        const dataUrl = bufferToDataUrl(
                            resp.response,
                            contentType
                        );
                        resolve({ dataUrl, index, source: "backend" });
                    } catch (e) {
                        console.warn(
                            `ADallower: failed to parse replacement index ${index}, using fallback`,
                            e
                        );
                        resolve({
                            dataUrl: FALLBACK_PIXEL,
                            index,
                            source: "fallback",
                        });
                    }
                },
                onerror: () => {
                    console.warn(
                        `ADallower: error fetching replacement index ${index}, using fallback`
                    );
                    resolve({
                        dataUrl: FALLBACK_PIXEL,
                        index,
                        source: "fallback",
                    });
                },
                ontimeout: () => {
                    console.warn(
                        `ADallower: timeout fetching replacement index ${index}, using fallback`
                    );
                    resolve({
                        dataUrl: FALLBACK_PIXEL,
                        index,
                        source: "fallback",
                    });
                },
            });
        });

        cachedImages[index] = imageResponse;
        return imageResponse;
    }

    /**
     * use the headers to check if it's an image request
     */
    function headerAccept(headers) {
        if (!headers) return null;
        try {
            if (headers.get) return headers.get("accept");
        } catch (_) {
            /* ignore */
        }
        if (Array.isArray(headers)) {
            const match = headers.find(
                ([k]) => String(k).toLowerCase() === "accept"
            );
            return match ? match[1] : null;
        }
        if (typeof headers === "object")
            return headers.accept || header.Accept || null;
        return null;
    }

    /**
     * check if the request looks like an image request
     */
    function looksLikeImageRequest(input, init) {
        const url = typeof input === "string" ? input : input?.url;
        if (url) {
            try {
                const { pathname } = new URL(url, location.href);
                if (IMAGE_EXT_RE.test(pathname)) return true;
            } catch (_) {
                /* ignore parse errors */
            }
        }

        const accept =
            headerAccept(init?.headers) || headerAccept(input?.headers);
        return (
            typeof accept === "string" && accept.toLowerCase().includes("image")
        );
    }

    // patch the page's fetch function to intercept image requests
    const originalFetch = window.fetch;
    window.fetch = function (input, init) {
        const url = typeof input === "string" ? input : input?.url;

        if (
            looksLikeImageRequest(input, init) &&
            (!ONLY_ADS || (url && isAdUrl(url)))
        ) {
            return fetchReplacement().then(({ dataUrl }) => {
                return originalFetch(dataUrl, init);
            });
        }

        return originalFetch(input, init);
    };

    const OriginalXHR = window.XMLHttpRequest;
    class RedirectingXHR extends OriginalXHR {
        open(method, url, user, password, async = true) {
            const shouldReplace =
                looksLikeImageRequest(url) && (!ONLY_ADS || isAdUrl(url));

            const target = shouldReplace ? `${BACKEND}/image/${Math.floor(Math.random() * totalImages)}` : url;

            return super.open(method, target, async, user, password);
        }
    }
    window.XMLHttpRequest = RedirectingXHR;
    let BLOCKLIST = [];
    GM_xmlhttpRequest({
        method: "GET",
        url: "https://raw.githubusercontent.com/sjhgvr/oisd/main/domainswild2_small.txt",
        onload: function (response) {
            BLOCKLIST = response.responseText
                .split("\n")
                .map((l) => l.trim())
                .filter((l) => l && !l.startsWith("#"));

            // Re-scan images now that ads are detectable
            document.querySelectorAll("img").forEach((img) => {
                delete img.dataset.processed;
                handleImage(img);
            });
        },
    });

    /**
     * to replace or not to replace :P
     */
    function handleImage(img) {
        if (!img || !img.src) return;

        const url = img.src;

        // no infnite loops here smh
        if (img.dataset.processed) return;

        // If ONLY_ADS is on but blocklist isn't ready, WAIT
        if (ONLY_ADS && BLOCKLIST.length === 0) return;

        img.dataset.processed = "true";

        // if ONLY_ADS is enabled, check against blocklist (otherwise always replace)
        let parsedUrl;
        try {
            parsedUrl = new URL(url, location.href);
        } catch {
            return;
        }
        const shouldReplace = !ONLY_ADS || isAdUrl(url);

        if (shouldReplace) {
            fetchReplacement().then(({ dataUrl, index, source }) => {
                if (!img.isConnected) return;
                img.src = dataUrl;
            });
        }
    }

    /**
     * intercept images while they load
     */
    function interceptBeforeLoad(event) {
        const node = event.target;
        if (!(node instanceof HTMLImageElement)) return;
        if (!node.src) return;
        if (node.dataset.processed) return;

        const shouldReplace = !ONLY_ADS || isAdUrl(node.src);
        if (!shouldReplace) return;

        event.preventDefault();
        node.dataset.processed = "true";

        fetchReplacement().then(({ dataUrl }) => {
            if (!node.isConnected) return;
            node.src = dataUrl;
        });
    }

    // the all seeing eye (that slows down your browser)
    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
                if (!(node instanceof HTMLElement)) continue;

                replaceBackgroundAds(node);

                node.querySelectorAll?.("*").forEach(replaceBackgroundAds);

                if (node.tagName === "IMG") {
                    handleImage(node);
                }

                node.querySelectorAll?.("img").forEach(handleImage);

                if (node.tagName === "IFRAME") {
                    handleIframe(node);
                }

                node.querySelectorAll?.("iframe").forEach(handleIframe);
            }
        }
    });

    observer.observe(document.documentElement, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ["src", "srcset"],
    });

    window.addEventListener("beforeload", interceptBeforeLoad, true);

    // scan on load
    document.querySelectorAll("img").forEach(handleImage);
})();