// ==UserScript==
// @name ReNXEnhanced
// @namespace https://github.com/origamiofficial/ReNXEnhanced
// @version 1.1
// @description A lightweight Tampermonkey script for importing and exporting NextDNS configuration profiles
// @author OrigamiOfficial
// @match https://my.nextdns.io/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Add styles for better UX
const style = document.createElement("style");
style.innerHTML = `
.list-group-item:hover .btn { visibility: visible !important; }
.tooltipParent:hover .customTooltip { opacity: 1 !important; visibility: visible !important; }
.tooltipParent .customTooltip:hover { opacity: 0 !important; visibility: hidden !important; }
div:hover #counters { visibility: hidden !important; }
.list-group-item:hover input.description, input.description:focus { display: initial !important;}
.Logs .row > * { width: auto; }
`;
document.head.appendChild(style);
// Internal functions
function makeApiRequest(method, path, body) {
return new Promise(function(resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.open(method, "https://api.nextdns.io/profiles/" + location.href.split("/")[3] + "/" + path, true);
xhr.withCredentials = true;
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status < 300)
resolve(xhr.responseText);
else
reject(xhr.responseText);
}
};
xhr.send(body ? JSON.stringify(body) : null);
});
}
function allowDenyDomain(btn, listName) {
const domain = btn.parentElement.parentElement.querySelector("a").innerHTML;
const description = ReNXsettings.logsDomainDescriptions[domain] || "";
makeApiRequest("POST", listName, { id: domain, description: description }).then(function() {
btn.parentElement.parentElement.style.display = "none";
});
}
function hideLogEntry(btn) {
btn.parentElement.parentElement.style.display = "none";
}
function exportToFile(obj, fileName) {
const data = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(obj, null, 2));
const a = document.createElement("a");
a.setAttribute("href", data);
a.setAttribute("download", fileName);
a.click();
}
function createSpinner(btn) {
const spinner = document.createElement("span");
spinner.className = "spinner-border spinner-border-sm";
spinner.style = "margin-left: 5px;";
btn.appendChild(spinner);
}
function createPleaseWaitModal(text) {
const modal = document.createElement("div");
modal.className = "modal";
modal.style = "display: block; background: rgba(0,0,0,0.5);";
modal.innerHTML = `<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body" style="text-align: center;">
<span class="spinner-border spinner-border-sm" style="margin-right: 10px;"></span>
${text}...
</div>
</div>
</div>`;
document.body.appendChild(modal);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function saveSettings() {
localStorage.setItem("ReNXsettings", JSON.stringify(ReNXsettings));
}
function loadReNXsettings() {
return new Promise(function(resolve) {
ReNXsettings = JSON.parse(localStorage.getItem("ReNXsettings")) || {
logsDomainDescriptions: {},
privacyBlocklistsCounters: {},
allowlistDescriptions: {},
denylistDescriptions: {}
};
resolve();
});
}
// Main function
function main() {
if (/\/logs/i.test(location.href)) {
const waitForContent = setInterval(function() {
if (document.querySelector(".row-cols-md-2") != null) {
clearInterval(waitForContent);
const logsContainer = document.querySelector(".row-cols-md-2");
const countersDiv = document.createElement("div");
countersDiv.id = "counters";
countersDiv.style = "position: absolute; right: 0; margin: 10px; font-size: small; opacity: 0.5;";
logsContainer.parentElement.insertBefore(countersDiv, logsContainer);
let blockedCounter = 0;
let allowedCounter = 0;
let hiddenCounter = 0;
const observer = new MutationObserver(function(mutations) {
blockedCounter = 0;
allowedCounter = 0;
hiddenCounter = 0;
document.querySelectorAll(".col").forEach(function(logEntry) {
if (logEntry.style.display == "none") {
hiddenCounter++;
return;
}
if (logEntry.querySelector(".text-danger"))
blockedCounter++;
else
allowedCounter++;
});
countersDiv.innerHTML = "Blocked: " + blockedCounter + " | Allowed: " + allowedCounter + " | Hidden: " + hiddenCounter;
});
observer.observe(logsContainer, { childList: true, subtree: true });
setInterval(function() {
document.querySelectorAll(".col").forEach(function(logEntry) {
if (logEntry.querySelector(".btn-group"))
return;
const btnGroup = document.createElement("div");
btnGroup.className = "btn-group btn-group-sm";
btnGroup.style = "position: absolute; right: 0; visibility: hidden;";
const allowBtn = document.createElement("button");
allowBtn.className = "btn btn-success";
allowBtn.innerHTML = "Allow";
allowBtn.onclick = function() { allowDenyDomain(this, "allowlist"); };
const denyBtn = document.createElement("button");
denyBtn.className = "btn btn-danger";
denyBtn.innerHTML = "Deny";
denyBtn.onclick = function() { allowDenyDomain(this, "denylist"); };
const hideBtn = document.createElement("button");
hideBtn.className = "btn btn-dark";
hideBtn.innerHTML = "Hide";
hideBtn.onclick = function() { hideLogEntry(this); };
btnGroup.appendChild(allowBtn);
btnGroup.appendChild(denyBtn);
btnGroup.appendChild(hideBtn);
logEntry.appendChild(btnGroup);
const domain = logEntry.querySelector("a").innerHTML;
const tooltipParent = document.createElement("div");
tooltipParent.className = "tooltipParent";
tooltipParent.style = "display: contents;";
tooltipParent.innerHTML = domain;
const tooltip = document.createElement("div");
tooltip.className = "customTooltip text-muted small";
tooltip.style = "position: absolute; z-index: 1; top: 25px; background: #000; color: #fff; padding: 5px; border-radius: 5px; opacity: 0; visibility: hidden; transition: opacity .2s;";
tooltip.innerHTML = ReNXsettings.logsDomainDescriptions[domain] || "";
tooltipParent.appendChild(tooltip);
logEntry.querySelector("a").innerHTML = "";
logEntry.querySelector("a").appendChild(tooltipParent);
});
}, 1000);
}
}, 500);
} else if (/privacy$/.test(location.href)) {
const waitForContent = setInterval(function() {
if (document.querySelector(".card-body") != null) {
clearInterval(waitForContent);
document.querySelectorAll(".list-group-item").forEach(function(item) {
const switchInput = item.querySelector("input[type=checkbox]");
if (!switchInput)
return;
const blocklistId = switchInput.id.match(/\d+/)[0];
const counterSpan = document.createElement("span");
counterSpan.className = "text-muted small";
counterSpan.style = "position: absolute; right: 70px;";
counterSpan.innerHTML = ReNXsettings.privacyBlocklistsCounters[blocklistId] || "0";
item.querySelector(".form-check").appendChild(counterSpan);
switchInput.onchange = function() {
ReNXsettings.privacyBlocklistsCounters[blocklistId] = "...";
saveSettings();
counterSpan.innerHTML = "...";
};
});
}
}, 500);
} else if (/security$/.test(location.href)) {
const waitForContent = setInterval(function() {
if (document.querySelector(".card-body") != null) {
clearInterval(waitForContent);
document.querySelectorAll(".form-check").forEach(function(item) {
const switchInput = item.querySelector("input[type=checkbox]");
if (!switchInput || switchInput.id.includes("web3"))
return;
const tooltipParent = document.createElement("div");
tooltipParent.className = "tooltipParent";
tooltipParent.style = "display: contents;";
tooltipParent.innerHTML = item.querySelector("label").innerHTML;
const tooltip = document.createElement("div");
tooltip.className = "customTooltip text-muted small";
tooltip.style = "position: absolute; z-index: 1; top: 25px; background: #000; color: #fff; padding: 5px; border-radius: 5px; opacity: 0; visibility: hidden; transition: opacity .2s;";
tooltip.innerHTML = switchInput.checked ? "Enabled" : "Disabled";
tooltipParent.appendChild(tooltip);
item.querySelector("label").innerHTML = "";
item.querySelector("label").appendChild(tooltipParent);
switchInput.onchange = function() {
tooltip.innerHTML = this.checked ? "Enabled" : "Disabled";
};
});
}
}, 500);
} else if (/allowlist$|denylist$/.test(location.href)) {
const waitForContent = setInterval(function() {
if (document.querySelector(".card-body") != null) {
clearInterval(waitForContent);
const listName = /allowlist$/.test(location.href) ? "allowlist" : "denylist";
document.querySelectorAll(".list-group-item").forEach(function(item) {
const domain = item.querySelector("span").innerHTML.match(/[^>]+$/)[0];
const descriptionInput = document.createElement("input");
descriptionInput.type = "text";
descriptionInput.className = "description form-control form-control-sm";
descriptionInput.placeholder = "Description";
descriptionInput.style = "display: none; position: absolute; right: 40px; width: 200px;";
descriptionInput.value = ReNXsettings[listName + "Descriptions"][domain] || "";
descriptionInput.onchange = function() {
ReNXsettings[listName + "Descriptions"][domain] = this.value;
saveSettings();
};
item.appendChild(descriptionInput);
});
setInterval(function() {
document.querySelectorAll(".list-group-item").forEach(function(item) {
if (item.querySelector(".btn-danger"))
return;
const domain = item.querySelector("span").innerHTML.match(/[^>]+$/)[0];
const deleteBtn = document.createElement("button");
deleteBtn.className = "btn btn-danger btn-sm";
deleteBtn.innerHTML = "Delete";
deleteBtn.style = "position: absolute; right: 0;";
deleteBtn.onclick = function() {
makeApiRequest("DELETE", listName + "/" + domain).then(function() {
item.remove();
delete ReNXsettings[listName + "Descriptions"][domain];
saveSettings();
});
};
item.appendChild(deleteBtn);
});
}, 1000);
}
}, 500);
} else if (/settings$/.test(location.href)) {
const waitForContent = setInterval(function() {
if (document.querySelector(".card-body") != null) {
clearInterval(waitForContent);
const exportConfigButton = document.createElement("button");
exportConfigButton.className = "btn btn-primary";
exportConfigButton.innerHTML = "Export this config";
exportConfigButton.onclick = function() {
const config = {};
const pages = ["security", "privacy", "parentalcontrol", "denylist", "allowlist", "settings", "rewrites"];
const configName = this.parentElement.previousSibling.querySelector("input").value;
let numPagesExported = 0;
createSpinner(this);
for (let i = 0; i < pages.length; i++) {
makeApiRequest("GET", pages[i]).then(function(response) {
config[pages[i]] = JSON.parse(response).data;
numPagesExported++;
if (numPagesExported == pages.length) {
config.privacy.blocklists = config.privacy.blocklists.map(b => ({ id: b.id }));
config.rewrites = config.rewrites.map(r => ({ name: r.name, content: r.content }));
config.parentalcontrol.services = config.parentalcontrol.services.map(s => ({ id: s.id, active: s.active, recreation: s.recreation }));
const fileName = configName + "-" + location.href.split("/")[3] + "-Export.json";
exportToFile(config, fileName);
exportConfigButton.lastChild.remove();
}
});
}
};
const importConfigButton = document.createElement("button");
importConfigButton.className = "btn btn-primary";
importConfigButton.innerHTML = "Import a config";
importConfigButton.onclick = function() { this.nextSibling.click(); };
const fileConfigInput = document.createElement("input");
fileConfigInput.type = "file";
fileConfigInput.style = "display: none;";
fileConfigInput.onchange = function() {
const file = new FileReader();
file.onload = async function() {
const config = JSON.parse(this.result);
const numItemsImported = { denylist: 0, allowlist: 0, rewrites: 0 };
const numFinishedRequests = { denylist: 0, allowlist: 0, rewrites: 0 };
const importIndividualItems = async function(listName) {
let listObj = config[listName];
listObj.reverse();
for (let i = 0; i < listObj.length; i++) {
await sleep(1000);
const item = listObj[i];
makeApiRequest("POST", listName, item)
.then(function(response) {
if (!response.includes('"error') || response.includes("duplicate") || response.includes("conflict")) {
numItemsImported[listName]++;
}
})
.catch(function(response) {
console.error(`Error importing ${listName} item:`, response);
})
.finally(function() {
numFinishedRequests[listName]++;
});
}
};
try {
console.log("Importing security settings...");
await makeApiRequest("PATCH", "security", config.security);
console.log("Security settings imported.");
} catch (error) {
console.error("Error importing security settings:", error);
}
try {
console.log("Importing privacy settings...");
await makeApiRequest("PATCH", "privacy", config.privacy);
console.log("Privacy settings imported.");
} catch (error) {
console.error("Error importing privacy settings:", error);
}
if (config.parentalcontrol) {
const parentalControlData = {
safeSearch: config.parentalcontrol.safeSearch,
youtubeRestrictedMode: config.parentalcontrol.youtubeRestrictedMode,
blockBypass: config.parentalcontrol.blockBypass,
services: config.parentalcontrol.services ? config.parentalcontrol.services.map(service => ({ id: service.id, active: service.active })) : [],
categories: config.parentalcontrol.categories ? config.parentalcontrol.categories.map(category => ({ id: category.id, active: category.active })) : []
};
try {
console.log("Importing parental control settings...");
await makeApiRequest("PATCH", "parentalcontrol", parentalControlData);
console.log("Parental control settings imported.");
} catch (error) {
console.error("Error importing parental control settings:", error);
}
}
try {
console.log("Importing settings...");
await makeApiRequest("PATCH", "settings", config.settings);
console.log("Settings imported.");
} catch (error) {
console.error("Error importing settings:", error);
}
importIndividualItems("rewrites");
importIndividualItems("denylist");
importIndividualItems("allowlist");
setInterval(function() {
if (numFinishedRequests.denylist === config.denylist.length &&
numFinishedRequests.allowlist === config.allowlist.length &&
numFinishedRequests.rewrites === config.rewrites.length) {
console.log("All import requests have finished.");
console.log(`Imported items - Denylist: ${numItemsImported.denylist}/${config.denylist.length}, ` +
`Allowlist: ${numItemsImported.allowlist}/${config.allowlist.length}, ` +
`Rewrites: ${numItemsImported.rewrites}/${config.rewrites.length}`);
setTimeout(() => location.reload(), 1000);
}
}, 1000);
};
file.readAsText(this.files[0]);
createPleaseWaitModal("Importing configuration");
};
const container = document.createElement("div");
container.style = "display: flex; grid-gap: 20px; margin-top: 20px;";
container.appendChild(exportConfigButton);
container.appendChild(importConfigButton);
container.appendChild(fileConfigInput);
document.querySelector(".card-body").appendChild(container);
}
}, 500);
}
}
// Load settings and run main function
let ReNXsettings;
loadReNXsettings().then(() => {
main();
let currentPage = location.href;
setInterval(function() {
if (currentPage !== location.href) {
currentPage = location.href;
main();
}
}, 250);
});
})();