HaGeZi to NextDNS

A userscript that allows you to fetch HaGeZi lists and add them to your NextDNS allowlist and denylist.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name            HaGeZi to NextDNS
// @namespace       vietthe.dev
// @match           https://my.nextdns.io/*
// @grant           GM_addStyle
// @version         1.1.5
// @license         MIT
// @author          vietthedev
// @compatible      firefox Violentmonkey
// @compatible      firefox Tampermonkey
// @compatible      chrome Violentmonkey
// @compatible      chrome Tampermonkey
// @compatible      opera Violentmonkey
// @compatible      opera Tampermonkey
// @compatible      safari Stay
// @compatible      edge Violentmonkey
// @compatible      edge Tampermonkey
// @compatible      brave Violentmonkey
// @compatible      brave Tampermonkey
// @description     A userscript that allows you to fetch HaGeZi lists and add them to your NextDNS allowlist and denylist.
// @description:en  A userscript that allows you to fetch HaGeZi lists and add them to your NextDNS allowlist and denylist.
// @description:vi  Userscript giúp bạn nhập các danh sách HaGeZi vào danh sách cho phép và danh sách chặn của NextDNS.
// @icon            data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAFOklEQVRogbXaXaicRxkH8N/sJmnSlOZIDC1CJRCseETFK72qFBpFvJQUpAptFdorL0pKkV540AvxQm8sokdsY6ISjNVCxSBUiSLRoq0XYpEqGGmbUGxPTmKO52N35/Fi93zsnvdrPzIw7Owzzzzz/z/zzLwz77zJzUgL0TLvsDV3oKfnigddk1LMuqvWrA2CY+6x7qzkT3hR26IfmL8ZXaWZWzwTn8ZTuENIAiEL/xQ+4+H08iy7mx2Bp+OIPb4geVI4aDNYYpD75beFx6x4zhfT9Vl0Oz2BZ2JO23E8LNyLW0rA93NyXfYLnHLI792fVqfpfnICp+Og5H7hIXxIuL0UdHH5LbyoZ9H7/NK9qTsJjAoCkTzvgGWHtOyT7ZW8U/JB4T7huDDXEOxweff/K8J5PRdkf9N2TdbBumXXLKS18Qn8KD6BE/iA7DDmMCe0xwZcDX5YRkd2VVgW/iP7M844mV5qTuCH8ajkq8JhMdAZ7qQZ6NyAyKjOqDzLuIzPeTxdqCdwOj4ieU5yZ4F3xvfwaF2ZrE6e/V1y3BPp9Z1whx9kC7FH8knhiDzwQoz8VpXr6spkVfLt/G49x42kYQLz9uOY0N5iv2l8NBeBHRd8kZ2iNj2EA8IxJ6K9E/KeEULtgWI/jRMelMfzOOFSXp9kt3qH1oBSAYEVaUs27iozC/BldrZz27uG5+0wgX2SDa3K1WMWICcB369vu1pFYFWStG7aSjJuKO2uS94zhHjXHCAGBMYBT/16Pj34PralqhFYk9wibc3+nUaqQE4aElGgU01q13NrmMB+oTswebNCosoJuUKnn7NLVQQ2hKQ781AZZ7Sq7CRdR7c0Cgi0ZV0bMwdfpLMpG8/O+g6tAgIHdF23OnWoVOnsBNpk1LbbBFarCSzrym5IDUDWdTwt+FGd/q70hoVUQWBJx5yrQtg+kE8Wy+NO5HqdDWHZSBrZjaYsW5L1SjdWUSJvstuMCXX65Q3J26MEdj/IuCJbE25r7LEmYVVmp4n9ft2acKWeQM9lYVW4bQzjTSZhuW4z+arwRj2BrktaVoQjM/d2k/qyNsmSjsujcHe/WvyXN2SvV56OymJ4+ABSd8Iqb1vc30WLqVNPYDF1ZL8unWDTgi+byGWTd1t+fhfWQgLQ8/OhlajOm+OuMKOAy+TbsiXrftOcQPZX2Uu1Ht+Zy4e+2NtFI1W+hJ51qvjlVjGB/vPgO3olwKYFWQV+d7iuyE4X4iwlAD0vyF6p9FLR6JSF06i8rrydL2j5x/gE9npTeFbWaTxJm8qL7BTpcEN41l27txCbqfrt9JfibuFXsqMMDNPsuVCmX9Reaf1FLZ9yKpUSqL5i+lp6Vfb1rVEo8lLRsNeF0GjbYof8FyerwNcTgGXfF87I8kThUka8uryq5ytOpz/UwasnsJg6woLsd7Ugy1eScSZyFn5i3fdqsRnnhuax+KiuU8J7G8dzVXwXl0O4KDzgx+nfTWA1v2b9Zvqj8Ijwl6GVZLpQGfY854WHmoJnkjuyR2Jey7eFe+w8tTGp16EnnNXxhHNp15a5Ko1/0f3d9IrwgOynspXa1aR+tVmSfcucR8cFzzS3lCfigDmfHYTVh0XBO9Wd/43I6Ai/xVPu9vzoYb1pmu6eeCFaXnOX8HnZSQZ3C1XA+79vCl+238884y0m/4Zidjf1D8ZR2ZP4uHAn9o2A/p/+kfCcjm84l5Zm0e1sv5U4EW0Hzeu5T/YxvF/o4mXZBXu94IxL03h8NM3+Yw/6oXXJ7TYckmW3Wva0G7MEvpn+DxZzjNuWWt/KAAAAAElFTkSuQmCC
// @homepageURL     https://github.com/vietthedev/hagezi-to-nextdns
// @supportURL      https://github.com/vietthedev/hagezi-to-nextdns/issues
// ==/UserScript==

/* jshint esversion:2020 */

(async (window) => {
	const punycode = await import("https://esm.run/punycode");
	const lang = {
		en: {
			hidePanel: "Hide Panel",
			showPanel: "Show Panel",
			numberOfBlockedTlds: "Number of blocked TLDs",
			numberOfDeniedDomains: "Number of denied domains",
			numberOfAllowedDomains: "Number of allowed domains",
			importHageziTlds: "Import HaGeZi TLDs",
			removeHageziTlds: "Remove HaGeZi TLDs",
			importHageziTldsAggressive: "Import HaGeZi TLDs (Aggressive)",
			removeHageziTldsAggressive: "Remove HaGeZi TLDs (Aggressive)",
			importHageziTldAllowlist: "Import HaGeZi TLD allowlist",
			removeHageziTldAllowlist: "Remove HaGeZi TLD allowlist",
			importErrorTrackers: "Import error trackers",
			removeErrorTrackers: "Remove error trackers",
			importCustomList: "Import custom list",
			removeCustomList: "Remove custom list",
			adding: "Adding",
			removing: "Removing",
			done: "Done!",
			expand: "Expand",
			collapse: "Collapse",
			chooseAListToProceed:
				"Choose a list to proceed:\n1. Allowlist\n2. Denylist",
			areYouSureYouWantToProceed: "Are you sure you want to proceed?",
			allowlist: "Allowlist",
			denylist: "Denylist",
			enterTheLink:
				"Enter the link to the list in wildcard asterisk or wildcard domain format:",
		},
		vi: {
			hidePanel: "Ẩn Bảng",
			showPanel: "Hiện Bảng",
			numberOfBlockedTlds: "Số lượng TLD đã chặn",
			numberOfDeniedDomains: "Số lượng tên miền đã chặn",
			numberOfAllowedDomains: "Số lượng tên miền đã cho phép",
			importHageziTlds: "Nhập TLD HaGeZi",
			removeHageziTlds: "Xoá TLD HaGeZi",
			importHageziTldsAggressive: "Nhập TLD HaGeZi (phiên bản mạnh hơn)",
			removeHageziTldsAggressive: "Xoá TLD HaGeZi (phiên bản mạnh hơn)",
			importHageziTldAllowlist: "Nhập danh sách cho phép HaGeZi TLD",
			removeHageziTldAllowlist: "Xoá danh sách cho phép HaGeZi TLD",
			importErrorTrackers: "Nhập trình theo dõi lỗi",
			removeErrorTrackers: "Xoá trình theo dõi lỗi",
			importCustomList: "Nhập danh sách tuỳ chọn",
			removeCustomList: "Xoá danh sách tuỳ chọn",
			adding: "Đang thêm",
			removing: "Đang xoá",
			done: "Xong!",
			expand: "Mở rộng",
			collapse: "Thu gọn",
			chooseAListToProceed:
				"Chọn danh sách để tiến hành:\n1. Danh sách cho phép\n2. Danh sách chặn",
			areYouSureYouWantToProceed: "Bạn có chắc muốn tiến hành?",
			allowlist: "Danh sách cho phép",
			denylist: "Danh sách chặn",
			enterTheLink:
				'Nhập đường dẫn tới danh sách tên miền ở định dạng có hoặc không có dấu "*":',
		},
	}[window.navigator.language === "vi" ? "vi" : "en"];
	const ListType = {
		Allowlist: "1",
		Denylist: "2",
	};

	const Utility = {
		sleep: (ms = 500) =>
			new Promise((resolve) => setTimeout(() => resolve(), ms)),
		toHex: (text) => {
			let hex = "";

			for (let i = 0; i < text.length; i++) {
				const charCode = text.charCodeAt(i);
				const hexCode = charCode.toString(16).padStart(2, "0");

				hex += hexCode;
			}

			return hex;
		},
	};

	const Helper = {
		getProfileId: () => location.pathname.split("/")[1],

		profileIdExists: () => location.pathname.split("/").length >= 3,

		confirmProceed: () => window.confirm(lang.areYouSureYouWantToProceed),

		promptListType: () => {
			let answer;

			while (answer !== null && !Object.values(ListType).includes(answer)) {
				answer = window.prompt(lang.chooseAListToProceed);
			}

			return answer;
		},

		promptUrl: () => {
			const pattern =
				/^(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/[a-zA-Z0-9]+\.[^\s]{2,}|[a-zA-Z0-9]+\.[^\s]{2,})$/;
			let answer;

			while (answer !== null && !pattern.test(answer)) {
				answer = window.prompt(lang.enterTheLink);
			}

			return answer;
		},

		getAdblockList: async (url, isTldList = false) => {
			const content = await fetch(url).then((res) => res.text());
			const matches = content
				.trim()
				.matchAll(/^(\|\|)?(?<domain>(xn--)?[\w.-]+)(\^)?$/gm);
			const domains = new Set();

			for (const match of matches) {
				if (isTldList && match.groups.domain.includes(".")) continue;
				domains.add(match.groups.domain);
			}

			return domains;
		},

		getWildcardList: async (url, isTldList = false) => {
			const content = await fetch(url).then((res) => res.text());
			const matches = content.trim().matchAll(/^(\*\.)*(?<domain>[\w.-]+)$/gm);
			const domains = new Set();

			for (const match of matches) {
				if (isTldList && match.groups.domain.includes(".")) continue;
				domains.add(match.groups.domain);
			}

			return domains;
		},

		getAbusedTlds: () =>
			Helper.getAdblockList(
				"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/spam-tlds-adblock.txt",
				true,
			),

		getAbusedTldsAggressive: () =>
			Helper.getWildcardList(
				"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/wildcard/spam-tlds-onlydomains.txt",
				true,
			),

		getTldAllowlist: () =>
			Helper.getWildcardList(
				"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/wildcard/spam-tlds-allow-onlydomains.txt",
			),

		getErrorTrackerList: () =>
			Helper.getWildcardList(
				"https://cdn.jsdelivr.net/gh/vietthedev/hagezi-to-nextdns@latest/resources/error-trackers-onlydomains.txt",
			),
	};

	class NextApi {
		apiHost = "https://api.nextdns.io";

		constructor(profileId) {
			this.profileId = profileId;
		}

		async request(path, options) {
			const response = await fetch(`${this.apiHost}${path}`, {
				headers: {
					"Content-Type": "application/json",
					...options?.headers,
				},
				mode: "cors",
				credentials: "include",
				...options,
			});

			if (!response.ok) {
				throw new Error(response.statusText);
			}

			return response;
		}

		async getSecurity() {
			const res = await this.request(`/profiles/${this.profileId}/security`);

			return await res.json();
		}

		async getDenylist() {
			const res = await this.request(`/profiles/${this.profileId}/denylist`);

			return await res.json();
		}

		async getAllowlist() {
			const res = await this.request(`/profiles/${this.profileId}/allowlist`);

			return await res.json();
		}

		addTld(tld) {
			return this.request(`/profiles/${this.profileId}/security/tlds`, {
				method: "POST",
				body: JSON.stringify({ id: tld }),
			});
		}

		removeTld(tld) {
			return this.request(
				`/profiles/${this.profileId}/security/tlds/hex:${Utility.toHex(tld)}`,
				{
					method: "DELETE",
				},
			);
		}

		denylistDomain(domain) {
			return this.request(`/profiles/${this.profileId}/denylist`, {
				method: "POST",
				body: JSON.stringify({ active: true, id: domain }),
			});
		}

		allowlistDomain(domain) {
			return this.request(`/profiles/${this.profileId}/allowlist`, {
				method: "POST",
				body: JSON.stringify({ active: true, id: domain }),
			});
		}

		removeDeniedDomain(domain) {
			return this.request(
				`/profiles/${this.profileId}/denylist/hex:${Utility.toHex(domain)}`,
				{
					method: "DELETE",
				},
			);
		}

		removeAllowedDomain(domain) {
			return this.request(
				`/profiles/${this.profileId}/allowlist/hex:${Utility.toHex(domain)}`,
				{
					method: "DELETE",
				},
			);
		}
	}

	const arrowDownHtml = `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M480-344 240-584l56-56 184 184 184-184 56 56-240 240Z"/></svg>`;
	const arrowUpHtml = `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M480-528 296-344l-56-56 240-240 240 240-56 56-184-184Z"/></svg>`;

	const hidePanel = () => {
		const panelContainer = document.querySelector(".panel-container");
		const toggleButton = document.querySelector(".toggle-button");

		requestAnimationFrame(function hide() {
			const { opacity } = getComputedStyle(panelContainer);

			if (opacity === "0") {
				panelContainer.classList.add("hidden");

				if (toggleButton) {
					toggleButton.textContent = lang.showPanel;
				}

				return;
			}

			panelContainer.style.opacity = Number.parseFloat(opacity) - 0.1;
			requestAnimationFrame(hide);
		});
	};

	const showPanel = () => {
		const panelContainer = document.querySelector(".panel-container");
		const toggleButton = document.querySelector(".toggle-button");

		panelContainer.classList.remove("hidden");

		requestAnimationFrame(function show() {
			const { opacity } = getComputedStyle(panelContainer);

			if (opacity === "1") {
				if (toggleButton) {
					toggleButton.textContent = lang.hidePanel;
				}
				return;
			}

			panelContainer.style.opacity = Number.parseFloat(opacity) + 0.1;
			requestAnimationFrame(show);
		});
	};

	const createUserInterface = () => {
		const toggleButton = document.createElement("button");
		const toggleButtonContainer = document.createElement("div");
		const expandButton = document.createElement("button");
		const card = document.createElement("div");
		const cardHeader = document.createElement("div");
		const cardBody = document.createElement("div");
		const cardFooter = document.createElement("div");
		const statList = document.createElement("ul");
		const controlContainer = document.createElement("div");
		const importHageziTldsButton = document.createElement("button");
		const removeHageziTldsButton = document.createElement("button");
		const importHageziTldsAggressiveButton = document.createElement("button");
		const removeHageziTldsAggressiveButton = document.createElement("button");
		const importHageziTldAllowlistButton = document.createElement("button");
		const removeHageziTldAllowlistButton = document.createElement("button");
		const importErrorTrackersButton = document.createElement("button");
		const removeErrorTrackersButton = document.createElement("button");
		const importCustomListButton = document.createElement("button");
		const removeCustomListButton = document.createElement("button");
		const progress = document.createElement("p");
		const loader = document.createElement("div");

		loader.classList.add("text-center");
		loader.innerHTML = `<div class="loader" />`;

		statList.classList.add("stat-list");

		expandButton.classList.add("btn", "panel-expand-button");
		expandButton.type = "button";
		expandButton.innerHTML = arrowDownHtml;
		expandButton.title = lang.expand;

		controlContainer.classList.add("panel-control-container", "hidden");

		importHageziTldsButton.classList.add("btn", "btn-primary", "btn-sm");
		removeHageziTldsButton.classList.add("btn", "btn-danger", "btn-sm");
		importHageziTldsAggressiveButton.classList.add(
			"btn",
			"btn-primary",
			"btn-sm",
		);
		removeHageziTldsAggressiveButton.classList.add(
			"btn",
			"btn-danger",
			"btn-sm",
		);
		importHageziTldAllowlistButton.classList.add(
			"btn",
			"btn-primary",
			"btn-sm",
		);
		removeHageziTldAllowlistButton.classList.add("btn", "btn-danger", "btn-sm");
		importErrorTrackersButton.classList.add("btn", "btn-primary", "btn-sm");
		removeErrorTrackersButton.classList.add("btn", "btn-danger", "btn-sm");
		importCustomListButton.classList.add("btn", "btn-primary", "btn-sm");
		removeCustomListButton.classList.add("btn", "btn-danger", "btn-sm");

		importHageziTldsButton.type = "button";
		removeHageziTldsButton.type = "button";
		importHageziTldsAggressiveButton.type = "button";
		removeHageziTldsAggressiveButton.type = "button";
		importHageziTldAllowlistButton.type = "button";
		removeHageziTldAllowlistButton.type = "button";
		importErrorTrackersButton.type = "button";
		removeErrorTrackersButton.type = "button";
		importCustomListButton.type = "button";
		removeCustomListButton.type = "button";

		importHageziTldsButton.textContent = lang.importHageziTlds;
		removeHageziTldsButton.textContent = lang.removeHageziTlds;
		importHageziTldsAggressiveButton.textContent =
			lang.importHageziTldsAggressive;
		removeHageziTldsAggressiveButton.textContent =
			lang.removeHageziTldsAggressive;
		importHageziTldAllowlistButton.textContent = lang.importHageziTldAllowlist;
		removeHageziTldAllowlistButton.textContent = lang.removeHageziTldAllowlist;
		importErrorTrackersButton.textContent = lang.importErrorTrackers;
		removeErrorTrackersButton.textContent = lang.removeErrorTrackers;
		importCustomListButton.textContent = lang.importCustomList;
		removeCustomListButton.textContent = lang.removeCustomList;

		controlContainer.appendChild(importHageziTldsButton);
		controlContainer.appendChild(removeHageziTldsButton);
		controlContainer.appendChild(importHageziTldsAggressiveButton);
		controlContainer.appendChild(removeHageziTldsAggressiveButton);
		controlContainer.appendChild(importHageziTldAllowlistButton);
		controlContainer.appendChild(removeHageziTldAllowlistButton);
		controlContainer.appendChild(importErrorTrackersButton);
		controlContainer.appendChild(removeErrorTrackersButton);
		controlContainer.appendChild(importCustomListButton);
		controlContainer.appendChild(removeCustomListButton);

		progress.classList.add("panel-progress");

		card.classList.add("card", "panel-container", "hidden");
		card.style.opacity = "0";

		cardHeader.classList.add("card-header");
		cardHeader.innerHTML = `<h6 class="mb-0 text-center">HaGeZi to NextDNS</h6>`;

		cardBody.classList.add("card-body");
		cardBody.appendChild(loader);
		cardBody.appendChild(statList);
		cardBody.appendChild(progress);

		cardFooter.classList.add("card-footer", "panel-footer");
		cardFooter.appendChild(expandButton);
		cardFooter.appendChild(controlContainer);

		toggleButtonContainer.classList.add(
			"nav-item",
			"d-flex",
			"align-items-center",
			"toggle-button-container",
		);

		toggleButton.classList.add("btn", "btn-primary", "toggle-button");
		toggleButton.textContent = toggleButton.classList.contains("hidden")
			? lang.showPanel
			: lang.hidePanel;
		toggleButton.type = "button";

		toggleButton.addEventListener("click", () => {
			const panelContainer = document.querySelector(".panel-container");

			if (!panelContainer.classList.contains("hidden")) {
				hidePanel();
				return;
			}

			showPanel();
		});

		card.appendChild(cardHeader);
		card.appendChild(cardBody);
		card.appendChild(cardFooter);
		toggleButtonContainer.appendChild(toggleButton);
		document.body.appendChild(card);

		return {
			card,
			cardBody,
			cardFooter,
			loader,
			progress,
			statList,
			expandButton,
			controlContainer,
			toggleButtonContainer,
			importHageziTldsButton,
			removeHageziTldsButton,
			importHageziTldsAggressiveButton,
			removeHageziTldsAggressiveButton,
			importHageziTldAllowlistButton,
			removeHageziTldAllowlistButton,
			importErrorTrackersButton,
			removeErrorTrackersButton,
			importCustomListButton,
			removeCustomListButton,
		};
	};

	const {
		cardBody,
		loader,
		progress,
		statList,
		expandButton,
		controlContainer,
		toggleButtonContainer,
		importHageziTldsButton,
		removeHageziTldsButton,
		importHageziTldsAggressiveButton,
		removeHageziTldsAggressiveButton,
		importHageziTldAllowlistButton,
		removeHageziTldAllowlistButton,
		importErrorTrackersButton,
		removeErrorTrackersButton,
		importCustomListButton,
		removeCustomListButton,
	} = createUserInterface();

	const addToggleButton = () => {
		const observer = new MutationObserver(() => {
			const nav = document.querySelector(".nav");

			if (!nav || nav.querySelector(".toggle-button")) return;

			nav.appendChild(toggleButtonContainer);
		});

		observer.observe(document.body, {
			childList: true,
			subtree: true,
		});
	};

	const updateLoadingState = (loading) => {
		if (loading) {
			importHageziTldsButton.disabled = true;
			removeHageziTldsButton.disabled = true;
			importHageziTldsAggressiveButton.disabled = true;
			removeHageziTldsAggressiveButton.disabled = true;
			importHageziTldAllowlistButton.disabled = true;
			removeHageziTldAllowlistButton.disabled = true;
			importErrorTrackersButton.disabled = true;
			removeErrorTrackersButton.disabled = true;
			importCustomListButton.disabled = true;
			removeCustomListButton.disabled = true;

			return;
		}

		importHageziTldsButton.disabled = false;
		removeHageziTldsButton.disabled = false;
		importHageziTldsAggressiveButton.disabled = false;
		removeHageziTldsAggressiveButton.disabled = false;
		importHageziTldAllowlistButton.disabled = false;
		removeHageziTldAllowlistButton.disabled = false;
		importErrorTrackersButton.disabled = false;
		removeErrorTrackersButton.disabled = false;
		importCustomListButton.disabled = false;
		removeCustomListButton.disabled = false;
	};

	const refreshStats = async () => {
		const profileId = Helper.getProfileId();
		const nextApi = new NextApi(profileId);

		updateLoadingState(true);

		const [tlds, denylist, allowlist] = await Promise.all([
			nextApi
				.getSecurity()
				.then((res) =>
					res.data.tlds.map(({ id }) =>
						id.startsWith("xn--") ? punycode.toUnicode(id) : id,
					),
				),
			nextApi.getDenylist().then((res) => res.data.map(({ id }) => id)),
			nextApi.getAllowlist().then((res) => res.data.map(({ id }) => id)),
		]);
		const tldContent = new Blob([tlds.toSorted().join("\n")], {
			type: "text/plain;charset=utf-8",
		});
		const denylistContent = new Blob([denylist.toSorted().join("\n")], {
			type: "text/plain",
		});
		const allowlistContent = new Blob([allowlist.toSorted().join("\n")], {
			type: "text/plain",
		});

		updateLoadingState(false);

		statList.innerHTML = `
    <li>${lang.numberOfBlockedTlds}: <a href="${URL.createObjectURL(tldContent)}" target="_blank">${tlds.length}</a></li>
    <li>${lang.numberOfDeniedDomains}: <a href="${URL.createObjectURL(denylistContent)}" target="_blank">${denylist.length}</a></li>
    <li>${lang.numberOfAllowedDomains}: <a href="${URL.createObjectURL(allowlistContent)}" target="_blank">${allowlist.length}</a></li>
    `;
	};

	const attachButtonEvents = () => {
		expandButton.addEventListener("click", (event) => {
			const target = event.currentTarget;

			if (controlContainer.classList.contains("hidden")) {
				controlContainer.classList.remove("hidden");
				target.title = lang.collapse;
				target.innerHTML = arrowUpHtml;
				return;
			}

			controlContainer.classList.add("hidden");
			target.title = lang.expand;
			target.innerHTML = arrowDownHtml;
		});

		importHageziTldsButton.addEventListener("click", async () => {
			if (!Helper.confirmProceed()) return;

			updateLoadingState(true);

			const profileId = Helper.getProfileId();
			const nextApi = new NextApi(profileId);

			try {
				const [currentTlds, hageziTlds] = await Promise.all([
					nextApi
						.getSecurity()
						.then((res) => res.data.tlds.map(({ id }) => id)),
					Helper.getAbusedTlds(),
				]);
				const tldsToImport = hageziTlds.difference(new Set(currentTlds));
				let index = 1;

				for (const domain of tldsToImport) {
					progress.textContent = `${lang.adding} ${domain} (${index}/${tldsToImport.size})`;
					await nextApi.addTld(domain);
					await Utility.sleep();
					index++;
				}
			} catch (error) {
				progress.textContent = error.message;
				return;
			}

			await refreshStats();
			progress.textContent = lang.done;
			updateLoadingState(false);
		});

		removeHageziTldsButton.addEventListener("click", async () => {
			if (!Helper.confirmProceed()) return;

			updateLoadingState(true);

			const profileId = Helper.getProfileId();
			const nextApi = new NextApi(profileId);

			try {
				const [currentTlds, hageziTlds] = await Promise.all([
					nextApi
						.getSecurity()
						.then((res) => res.data.tlds.map(({ id }) => id)),
					Helper.getAbusedTlds(),
				]);
				const tldsToRemove = hageziTlds.intersection(new Set(currentTlds));
				let index = 1;

				for (const domain of tldsToRemove) {
					progress.textContent = `${lang.removing} ${domain} (${index}/${tldsToRemove.size})`;
					await nextApi.removeTld(domain);
					await Utility.sleep();
					index++;
				}
			} catch (error) {
				progress.textContent = error.message;
				return;
			}

			await refreshStats();
			progress.textContent = lang.done;
			updateLoadingState(false);
		});

		importHageziTldsAggressiveButton.addEventListener("click", async () => {
			if (!Helper.confirmProceed()) return;

			updateLoadingState(true);

			const profileId = Helper.getProfileId();
			const nextApi = new NextApi(profileId);

			try {
				const [currentTlds, hageziTlds] = await Promise.all([
					nextApi
						.getSecurity()
						.then((res) => res.data.tlds.map(({ id }) => id)),
					Helper.getAbusedTldsAggressive(),
				]);
				const tldsToImport = hageziTlds.difference(new Set(currentTlds));
				let index = 1;

				for (const domain of tldsToImport) {
					progress.textContent = `${lang.adding} ${domain} (${index}/${tldsToImport.size})`;
					await nextApi.addTld(domain);
					await Utility.sleep();
					index++;
				}
			} catch (error) {
				progress.textContent = error.message;
				return;
			}

			await refreshStats();
			progress.textContent = lang.done;
			updateLoadingState(false);
		});

		removeHageziTldsAggressiveButton.addEventListener("click", async () => {
			if (!Helper.confirmProceed()) return;

			updateLoadingState(true);

			const profileId = Helper.getProfileId();
			const nextApi = new NextApi(profileId);

			try {
				const [currentTlds, hageziTlds] = await Promise.all([
					nextApi
						.getSecurity()
						.then((res) => res.data.tlds.map(({ id }) => id)),
					Helper.getAbusedTldsAggressive(),
				]);
				const tldsToRemove = hageziTlds.intersection(new Set(currentTlds));
				let index = 1;

				for (const domain of tldsToRemove) {
					progress.textContent = `${lang.removing} ${domain} (${index}/${tldsToRemove.size})`;
					await nextApi.removeTld(domain);
					await Utility.sleep();
					index++;
				}
			} catch (error) {
				progress.textContent = error.message;
				return;
			}

			await refreshStats();
			progress.textContent = lang.done;
			updateLoadingState(false);
		});

		importHageziTldAllowlistButton.addEventListener("click", async () => {
			if (!Helper.confirmProceed()) return;

			updateLoadingState(true);

			const profileId = Helper.getProfileId();
			const nextApi = new NextApi(profileId);

			try {
				const [currentAllowlist, hageziTldAllowlist] = await Promise.all([
					nextApi.getAllowlist().then((res) => res.data.map(({ id }) => id)),
					Helper.getTldAllowlist(),
				]);
				const domainsToImport = hageziTldAllowlist.difference(
					new Set(currentAllowlist),
				);
				let index = 1;

				for (const domain of domainsToImport) {
					progress.textContent = `${lang.adding} ${domain} (${index}/${domainsToImport.size})`;
					await nextApi.allowlistDomain(domain);
					await Utility.sleep();
					index++;
				}
			} catch (error) {
				progress.textContent = error.message;
				return;
			}

			await refreshStats();
			progress.textContent = lang.done;
			updateLoadingState(false);
		});

		removeHageziTldAllowlistButton.addEventListener("click", async () => {
			if (!Helper.confirmProceed()) return;

			updateLoadingState(true);

			const profileId = Helper.getProfileId();
			const nextApi = new NextApi(profileId);

			try {
				const [currentAllowlist, hageziTldAllowlist] = await Promise.all([
					nextApi.getAllowlist().then((res) => res.data.map(({ id }) => id)),
					Helper.getTldAllowlist(),
				]);
				const domainsToRemove = hageziTldAllowlist.intersection(
					new Set(currentAllowlist),
				);
				let index = 1;

				for (const domain of domainsToRemove) {
					progress.textContent = `${lang.removing} ${domain} (${index}/${domainsToRemove.size})`;
					await nextApi.removeAllowedDomain(domain);
					await Utility.sleep();
					index++;
				}
			} catch (error) {
				progress.textContent = error.message;
				return;
			}

			await refreshStats();
			progress.textContent = lang.done;
			updateLoadingState(false);
		});

		importErrorTrackersButton.addEventListener("click", async () => {
			const listType = Helper.promptListType();

			if (!listType) return;

			if (!Helper.confirmProceed()) return;

			updateLoadingState(true);

			const profileId = Helper.getProfileId();
			const nextApi = new NextApi(profileId);

			try {
				const [currentList, errorTrackerList] = await Promise.all([
					listType === ListType.Allowlist
						? nextApi.getAllowlist().then((res) => res.data.map(({ id }) => id))
						: nextApi.getDenylist().then((res) => res.data.map(({ id }) => id)),
					Helper.getErrorTrackerList(),
				]);
				const domainsToImport = errorTrackerList.difference(
					new Set(currentList),
				);
				const processDomain =
					listType === ListType.Allowlist
						? nextApi.allowlistDomain.bind(nextApi)
						: nextApi.denylistDomain.bind(nextApi);
				let index = 1;

				for (const domain of domainsToImport) {
					progress.textContent = `${lang.adding} ${domain} (${index}/${domainsToImport.size})`;
					await processDomain(domain);
					await Utility.sleep();
					index++;
				}
			} catch (error) {
				progress.textContent = error.message;
				return;
			}

			await refreshStats();
			progress.textContent = lang.done;
			updateLoadingState(false);
		});

		removeErrorTrackersButton.addEventListener("click", async () => {
			const listType = Helper.promptListType();

			if (!listType) return;

			if (!Helper.confirmProceed()) return;

			updateLoadingState(true);

			const profileId = Helper.getProfileId();
			const nextApi = new NextApi(profileId);

			try {
				const [currentList, errorTrackerList] = await Promise.all([
					listType === ListType.Allowlist
						? nextApi.getAllowlist().then((res) => res.data.map(({ id }) => id))
						: nextApi.getDenylist().then((res) => res.data.map(({ id }) => id)),
					Helper.getErrorTrackerList(),
				]);
				const domainsToRemove = errorTrackerList.intersection(
					new Set(currentList),
				);
				const processDomain =
					listType === ListType.Allowlist
						? nextApi.removeAllowedDomain.bind(nextApi)
						: nextApi.removeDeniedDomain.bind(nextApi);
				let index = 1;

				for (const domain of domainsToRemove) {
					progress.textContent = `${lang.removing} ${domain} (${index}/${domainsToRemove.size})`;
					await processDomain(domain);
					await Utility.sleep();
					index++;
				}
			} catch (error) {
				progress.textContent = error.message;
				return;
			}

			await refreshStats();
			progress.textContent = lang.done;
			updateLoadingState(false);
		});

		importCustomListButton.addEventListener("click", async () => {
			const url = Helper.promptUrl();

			if (!url) return;

			const listType = Helper.promptListType();

			if (!listType) return;

			if (!Helper.confirmProceed()) return;

			updateLoadingState(true);

			const profileId = Helper.getProfileId();
			const nextApi = new NextApi(profileId);

			try {
				const [currentList, importingList] = await Promise.all([
					listType === ListType.Allowlist
						? nextApi.getAllowlist().then((res) => res.data.map(({ id }) => id))
						: nextApi.getDenylist().then((res) => res.data.map(({ id }) => id)),
					Helper.getWildcardList(url),
				]);
				const domainsToImport = importingList.difference(new Set(currentList));
				const processDomain =
					listType === ListType.Allowlist
						? nextApi.allowlistDomain.bind(nextApi)
						: nextApi.denylistDomain.bind(nextApi);
				let index = 1;

				for (const domain of domainsToImport) {
					progress.textContent = `${lang.adding} ${domain} (${index}/${domainsToImport.size})`;
					await processDomain(domain);
					await Utility.sleep();
					index++;
				}
			} catch (error) {
				progress.textContent = error.message;
				return;
			}

			await refreshStats();
			progress.textContent = lang.done;
			updateLoadingState(false);
		});

		removeCustomListButton.addEventListener("click", async () => {
			const url = Helper.promptUrl();

			if (!url) return;

			const listType = Helper.promptListType();

			if (!listType) return;

			if (!Helper.confirmProceed()) return;

			updateLoadingState(true);

			const profileId = Helper.getProfileId();
			const nextApi = new NextApi(profileId);

			try {
				const [currentList, removingList] = await Promise.all([
					listType === ListType.Allowlist
						? nextApi.getAllowlist().then((res) => res.data.map(({ id }) => id))
						: nextApi.getDenylist().then((res) => res.data.map(({ id }) => id)),
					Helper.getWildcardList(url),
				]);
				const domainsToImport = removingList.intersection(new Set(currentList));
				const processDomain =
					listType === ListType.Allowlist
						? nextApi.removeAllowedDomain.bind(nextApi)
						: nextApi.removeDeniedDomain.bind(nextApi);
				let index = 1;

				for (const domain of domainsToImport) {
					progress.textContent = `${lang.removing} ${domain} (${index}/${domainsToImport.size})`;
					await processDomain(domain);
					await Utility.sleep();
					index++;
				}
			} catch (error) {
				progress.textContent = error.message;
				return;
			}

			await refreshStats();
			progress.textContent = lang.done;
			updateLoadingState(false);
		});
	};

	addToggleButton();
	attachButtonEvents();

	let isInit = false;
	const init = async () => {
		if (!Helper.profileIdExists()) {
			hidePanel();
			return;
		}
		showPanel();
		addToggleButton();

		if (isInit) return;

		statList.innerHTML = "";
		cardBody.prepend(loader);
		await refreshStats();
		loader.remove();
		isInit = true;
	};

	window.navigation?.addEventListener("navigatesuccess", init);
	init();

	GM_addStyle(`
    .panel-container {
      position: fixed;
      right: 1rem;
      bottom: 1rem;
    }

    .toggle-button-container {
      justify-content: end;
      flex: 1;
    }

    .toggle-button {
      white-space: nowrap;
    }

    .stat-list {
      list-style: initial;
    }

    .panel-progress {
      margin: 0;
    }

    .panel-footer {
      display: flex;
      flex-direction: column;
      row-gap: .5rem;
    }

    .panel-control-container {
      display: flex;
      flex-direction: column;
      row-gap: .5rem;
    }

    .panel-control-container > .btn {
      width: fit-content;
    }

    .panel-footer > .panel-expand-button {
      display: flex;
      align-items: center;
      justify-content: center;
      width: auto;
      padding: 0;
    }

    .hidden {
      display: none;
    }

    .loader {
      width: 1.5rem;
      height: 1.5rem;
      border: 2px solid #000;
      border-bottom-color: transparent;
      border-radius: 50%;
      display: inline-block;
      box-sizing: border-box;
      animation: rotation 1s linear infinite;
    }

    @keyframes rotation {
      0% {
        transform: rotate(0deg);
      }

      100% {
        transform: rotate(360deg);
      }
    }
  `);
})(window);