// ==UserScript==
// @name Deeper Tools
// @description Набор инструментов для Deeper.
// @namespace http://tampermonkey.net/
// @version 4.1
// @author https://github.com/lReDragol
// @icon https://avatars.mds.yandex.net/get-socsnippets/10235467/2a0000019509580bc84108597cea65bc46ee/square_83
// @match http://34.34.34.34/*
// @match http://11.22.33.44/*
// @match *://*/*
// @license MIT
// @run-at document-start
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant GM_download
// @grant GM_addStyle
// ==/UserScript==
(function () {
'use strict';
const ALLOWED_HOSTS = new Set(['34.34.34.34', '11.22.33.44']);
const isAllowedHost = () => location.protocol === 'http:' && ALLOWED_HOSTS.has(location.hostname);
const palettes = {
default: {},
green: {
'--bg-primary': '#2a2a2a',
'--bg-secondary': '#383838',
'--bg-tertiary': '#454545',
'--text-primary': '#d0f4d0',
'--text-secondary': '#b0e8b0',
'--text-muted': '#8ac48a',
'--accent': '#4caf50',
'--border-primary': '#4d4d4d',
'--border-light': '#777777',
'--hover-bg': '#505050',
'--active-bg': '#626262',
'--highlight': '#6ee76e',
'--disabled-bg': '#2f2f2f'
},
red: {
'--bg-primary': '#2b1a1a',
'--bg-secondary': '#3d1f1f',
'--bg-tertiary': '#502525',
'--text-primary': '#ffe6e6',
'--text-secondary': '#ffb3b3',
'--text-muted': '#cc7f7f',
'--accent': '#ff4d4d',
'--border-primary': '#661010',
'--border-light': '#993333',
'--hover-bg': '#661515',
'--active-bg': '#7a1a1a',
'--highlight': '#ff7f7f',
'--disabled-bg': '#2f1c1c'
},
purple: {
'--bg-primary': '#1a1a2a',
'--bg-secondary': '#28283a',
'--bg-tertiary': '#35354b',
'--text-primary': '#c0c0e8',
'--text-secondary': '#9e9ede',
'--text-muted': '#8a8abf',
'--accent': '#e0e0f8',
'--border-primary': '#3d3d4d',
'--border-light': '#777787',
'--hover-bg': '#505050',
'--active-bg': '#626262',
'--highlight': '#8f8fdf',
'--disabled-bg': '#2f2f2f'
}
};
const themeNames = Object.keys(palettes);
let currentThemeIndex = GM_getValue('deeperThemeIndex', 0);
const CSS_BASE = `
* { background: transparent !important; color: var(--text-primary) !important; border-color: var(--border-primary) !important; }
*::before, *::after { background: transparent !important; }
html, body, [class*="bg-"], [style*="background"] { background-color: var(--bg-primary) !important; background-image: none !important; }
h1, h2, h3, h4, h5, h6, p, span, label, div, li { color: var(--text-primary) !important; }
a, a * { color: var(--accent) !important; }
table, thead, tbody, tr, th, td { background: var(--bg-tertiary) !important; color: var(--text-primary) !important; border-color: var(--border-primary) !important; }
button, input, select, textarea, .ant-btn, .ant-input, .ant-select-selector, .ant-input-affix-wrapper {
background: var(--bg-secondary) !important; color: var(--text-primary) !important; border: 1px solid var(--border-light) !important;
}
button:hover, .ant-btn:hover, input:hover, select:hover, textarea:hover, .ant-input:hover, .ant-select-selector:hover {
background: var(--hover-bg) !important;
}
button:active, .ant-btn:active { background: var(--active-bg) !important; }
button[disabled], input[disabled], select[disabled], textarea[disabled], .ant-btn[disabled], .ant-input[disabled] {
background: var(--disabled-bg) !important; color: var(--text-muted) !important; cursor: not-allowed !important; opacity: 0.6 !important;
}
.ant-layout, .ant-layout-header, .ant-layout-sider, .ant-layout-content, .ant-layout-footer {
background: var(--bg-primary) !important; color: var(--text-primary) !important;
}
.ant-card, .card, .panel { background: var(--bg-terтиary) !important; box-shadow: none !important; color: var(--text-primary) !important; }
.ant-menu, .ant-menu-item, .ant-menu-submenu, .ant-menu-item-group-title {
background: var(--bg-secondary) !important; color: var(--text-primary) !important;
}
.ant-modal-content, .ant-popover-inner-content, .ant-popover-title {
background: var(--bg-secondary) !important; color: var(--text-primary) !important; border-color: var(--border-primary) !important;
}
.ant-tooltip-inner { background: var(--bg-secondary) !important; color: var(--text-primary) !important; }
.ant-tabs-nav, .ant-tabs-tab, .ant-tabs-tab-active, .ant-tabs-content-holder {
background: var(--bg-secondary) !important; color: var(--text-primary) !important;
}
.ant-tag, .ant-tag-green, .ant-badge-status-success, .ant-badge-status-default {
background: var(--bg-tertiary) !important; color: var(--text-primary) !important;
}
.anticon, .anticon svg { color: var(--accent) !important; fill: var(--accent) !important; }
::-webkit-scrollbar { width: 8px; background: var(--bg-secondary); }
::-webkit-scrollbar-thumb { background: var(--border-primary); border-radius: 4px; }
::selection { background: var(--highlight) !important; color: var(--bg-primary) !important; }
.tm-sticky-controls { position: sticky; bottom: 0; z-index: 999; background: var(--bg-secondary); padding: 8px; display: flex; justify-content: space-between; align-items: center; }
.page-list-container { margin: 1.5rem 0; display: flex; flex-direction: column; gap: 0.75rem; }
.page-list-item { position: relative; padding: 1rem 3rem 1rem 1rem; background: var(--bg-tertiary); border: 1px solid var(--border-primary); border-radius: 0.5rem; }
.page-list-item .delete-button { position: absolute; top: 0.5rem; right: 0.5rem; background: transparent; border: none; color: var(--text-secondary); font-size: 1.25rem; line-height: 1; cursor: pointer; }
.page-list-item .delete-button:hover { color: var(--accent); }
`;
function applyTheme(idx) {
GM_setValue('deeperThemeIndex', idx);
const styleId = 'deeper-theme-style';
let styleEl = document.getElementById(styleId);
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = styleId;
document.head.appendChild(styleEl);
}
const name = themeNames[idx];
if (name === 'default') {
styleEl.textContent = '';
} else {
const pal = palettes[name];
const vars = Object.entries(pal).map(([k, v]) => `${k}: ${v};`).join('\n');
styleEl.textContent = `:root { ${vars} } ${CSS_BASE}`;
}
}
if (isAllowedHost()) applyTheme(currentThemeIndex);
function gmFetch(url, init = {}) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: init.method || 'GET',
url,
headers: init.headers || {},
data: init.body || null,
timeout: init.timeout || 30000,
onload: function (response) {
response.json = () => Promise.resolve(JSON.parse(response.responseText || 'null'));
resolve(response);
},
onerror: reject,
ontimeout: () => reject(new Error('GM_xmlhttpRequest: timeout'))
});
});
}
(function installAutoLogin(){
if (!isAllowedHost()) return;
function isLoginLike() {
try {
const p = location.pathname.replace(/\/+$/,'').toLowerCase();
if (p === '/login' || p.endsWith('/admin/login')) return true;
if (location.hash && /login/i.test(location.hash)) return true;
if (location.search && /login/i.test(location.search)) return true;
return false;
} catch { return false; }
}
// Захват логина/пароля из обычной формы (submit), если не XHR/fetch
function captureCredsFromForm() {
try {
const form = document.querySelector('form');
if (!form) return;
const userEl = form.querySelector('input[name="username"], input[type="text"], input[autocomplete="username"]');
const passEl = form.querySelector('input[type="password"], input[name="password"], input[autocomplete="current-password"]');
if (!passEl) return;
const store = () => {
const u = (userEl && userEl.value || 'admin').trim();
const p = (passEl.value || '').trim();
if (u) GM_setValue('adminUsername', u);
if (p) {
GM_setValue('adminPassword', p);
console.log('[Deeper Tools] Пароль сохранён из формы.');
}
};
form.addEventListener('submit', store, { capture: true });
passEl.addEventListener('change', store);
} catch (e) {
console.warn('[Deeper Tools] captureCredsFromForm error:', e);
}
}
async function doAutoLogin() {
const pwd = GM_getValue('adminPassword');
const uname = GM_getValue('adminUsername') || 'admin';
if (!pwd) {
console.log('[Deeper Tools] Автологин: нет сохранённых учётных данных.');
return;
}
try {
console.log('[Deeper Tools] Автологин: POST /api/admin/login …');
const r = await gmFetch(`${location.origin}/api/admin/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: uname, password: pwd })
});
const status = r.status;
let body = null;
try { body = await r.json(); } catch {}
console.log('[Deeper Tools] Автологин: статус', status, body);
if (status === 200) {
location.href = '/admin/dashboard';
} else {
console.warn('[Deeper Tools] Автологин не принят сервером. Проверьте username/password/endpoint.');
}
} catch (e) {
console.error('[Deeper Tools] Автологин ошибка:', e);
}
}
if (isLoginLike()) {
doAutoLogin();
window.addEventListener('load', doAutoLogin, { once: true });
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', captureCredsFromForm, { once: true });
} else {
captureCredsFromForm();
}
}
})();
/* ---------------------------------
* Пейджинг/агрегатор для whitelist/blacklist
* --------------------------------- */
const PAGING = { SIZE: 100, MAX_PAGES: 500 };
const isTargetEndpoint = (p) =>
p === '/api/smartRoute/getRoutingWhitelist/domain' ||
p === '/api/smartRoute/getRoutingBlacklist/domain';
function mustAggregateUrl(urlStr, method = 'GET') {
if (!isAllowedHost()) return false;
try {
const u = new URL(urlStr, location.href);
if (!isTargetEndpoint(u.pathname)) return false;
if (u.searchParams.get('__tm_bypass_all') === '1') return false;
return String(method || 'GET').toUpperCase() === 'GET';
} catch { return false; }
}
async function fetchAllPagesViaFetch(origFetch, baseUrl, init) {
const all = [];
let pageNo = 1;
while (pageNo <= PAGING.MAX_PAGES) {
const u = new URL(baseUrl);
u.searchParams.set('pageNo', String(pageNo));
u.searchParams.set('pageSize', String(PAGING.SIZE));
u.searchParams.set('__tm_bypass_all', '1');
const r = await origFetch(u.toString(), init);
const d = await r.json();
const list = Array.isArray(d.list) ? d.list : [];
if (list.length) all.push(...list);
if (list.length < PAGING.SIZE) return { template: d, list: all };
pageNo++;
}
return { template: {}, list: all };
}
function dedupList(list) {
const seen = new Set();
const out = [];
for (const it of list) {
const key = it?.id ?? it?.domain ?? it?.domainName ?? JSON.stringify(it);
if (!seen.has(key)) { seen.add(key); out.push(it); }
}
return out;
}
function defineRO(obj, prop, val) {
try { Object.defineProperty(obj, prop, { configurable: true, get: () => val }); } catch {}
}
/* ---------------------------------
* Улучшения UI списка доменов
* --------------------------------- */
function injectListCSS() {
if (!isAllowedHost()) return;
if (document.getElementById('dc-long-list-style')) return;
const style = document.createElement('style');
style.id = 'dc-long-list-style';
style.textContent = `
.ant-table-body, .ant-table-content, .ant-spin-nested-loading, .ant-spin-container {
max-height: none !important;
overflow: visible !important;
}
.ant-table table { table-layout: auto !important; }
`;
document.documentElement.appendChild(style);
}
function placeSearchNearLeftButtons() {
if (!isAllowedHost()) return;
const existingInput = document.getElementById('dc-domain-search');
if (existingInput && document.body.contains(existingInput)) return;
const allButtons = Array.from(document.querySelectorAll('button, .ant-btn'));
const addButtons = allButtons.filter(b => /Добавить/i.test(b.textContent || ''));
const importButtons= allButtons.filter(b => /Импорт/i.test(b.textContent || ''));
const exportButtons= allButtons.filter(b => /Экспорт/i.test(b.textContent || ''));
if (!addButtons.length) return;
let leftMostAddButton = addButtons[0];
let minLeft = Infinity;
for (const btn of addButtons) {
const r = btn.getBoundingClientRect();
if (r.left < minLeft) { minLeft = r.left; leftMostAddButton = btn; }
}
let container = leftMostAddButton;
for (let i = 0; i < 8 && container; i++) {
container = container.parentElement;
if (!container) break;
const texts = Array.from(container.querySelectorAll('button, .ant-btn')).map(el => (el.textContent || '').toLowerCase());
const hasAdd = texts.some(t => t.includes('добавить'));
const hasImport = texts.some(t => t.includes('импорт'));
const hasExport = texts.some(t => t.includes('экспорт'));
if (hasAdd && hasImport && hasExport) break;
}
if (!container) container = leftMostAddButton.parentElement || document.body;
const input = document.createElement('input');
input.id = 'dc-domain-search';
input.type = 'text';
input.placeholder = 'Поиск доменов...';
Object.assign(input.style, {
height: '40px',
padding: '0 12px',
border: '1px solid rgba(0,0,0,.25)',
borderRadius: '8px',
marginRight: '8px',
minWidth: '280px',
flex: '0 0 auto',
outline: 'none'
});
const cardOrTableRoot =
leftMostAddButton.closest('.ant-card, [class*="card"], .ant-table-wrapper') ||
container.closest('.ant-card, [class*="card"], .ant-table-wrapper') ||
document.body;
// функция фильтрации строк tbody
const applyFilter = () => {
const query = input.value.trim().toLowerCase();
const table = cardOrTableRoot.querySelector('table') || document.querySelector('table');
const tbody = table ? (table.tBodies && table.tBodies[0]) : cardOrTableRoot.querySelector('tbody');
if (!tbody) return;
tbody.querySelectorAll('tr').forEach(tr => {
const text = (tr.textContent || '').toLowerCase();
tr.style.display = !query || text.includes(query) ? '' : 'none';
});
};
input.addEventListener('input', applyFilter);
try {
const directParent = leftMostAddButton.parentElement;
if (directParent && directParent.contains(leftMostAddButton)) {
directParent.insertBefore(input, leftMostAddButton);
} else if (container && container.contains(leftMostAddButton)) {
container.insertBefore(input, container.firstChild || null);
} else {
leftMostAddButton.before(input);
}
} catch (e) {
console.warn('[Deeper Tools] insertBefore fallback:', e);
(leftMostAddButton.parentElement || container || document.body).prepend(input);
}
const tbody = (cardOrTableRoot.querySelector('table') || {}).tBodies?.[0] || cardOrTableRoot.querySelector('tbody');
if (tbody) {
new MutationObserver(applyFilter).observe(tbody, { childList: true, subtree: true });
}
}
function bootDomainPageHelpers() {
injectListCSS();
placeSearchNearLeftButtons();
}
/* ======================================================================
* Domain Scanner — ГЛОБАЛЬНЫЙ (доступен везде через пункт меню)
* ====================================================================== */
function getScannerEnabled() { return GM_getValue('domainScannerEnabled', false); }
function setScannerEnabled(val) {
GM_setValue('domainScannerEnabled', val);
updateScannerMenuCommand();
if (!val) {
const c = document.getElementById('domain-scanner-container');
if (c) c.remove();
} else {
ensureScannerContainer();
}
console.log('[Deeper Tools] Domain Scanner: ' + (val ? 'ON' : 'OFF'));
}
if (!window.__deeper_hooks_installed__) {
window.__deeper_hooks_installed__ = true;
// Перехват XHR
const nativeOpen = XMLHttpRequest.prototype.open;
const nativeSend = XMLHttpRequest.prototype.send;
const nativeSetRH = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.open = function (method, url) {
this._method = method;
this._headers = {};
try { this._urlObj = new URL(url, location.href); } catch (_) { this._urlObj = null; }
if (getScannerEnabled() && this._urlObj) { try { addDomain(this._urlObj.hostname); } catch {} }
return nativeOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.setRequestHeader = function (k, v) {
try { if (!this._headers) this._headers = {}; this._headers[k] = v; } catch {}
return nativeSetRH.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (body) {
// Захват логина/пароля из /api/admin/login (JSON, URL-encoded, FormData)
try {
if (
this._urlObj &&
isAllowedHost() &&
this._urlObj.pathname.startsWith('/api/admin/login') &&
String(this._method || '').toUpperCase() === 'POST'
) {
let pwd = null, uname = null;
if (typeof body === 'string') {
try {
if (body.trim().startsWith('{')) {
const p = JSON.parse(body);
pwd = p && p.password;
uname = p && (p.username || p.login || p.user);
} else {
const usp = new URLSearchParams(body);
pwd = usp.get('password');
uname = usp.get('username') || usp.get('login') || usp.get('user');
}
} catch {}
} else if (body && typeof body === 'object') {
try {
if (typeof body.get === 'function') {
pwd = body.get('password') || pwd;
uname = body.get('username') || body.get('login') || body.get('user') || uname;
}
} catch {}
}
if (uname) GM_setValue('adminUsername', uname);
if (pwd && !GM_getValue('adminPassword')) {
GM_setValue('adminPassword', pwd);
console.log('[Deeper Tools] Пароль сохранён из XHR.');
}
}
} catch {}
// Агрегатор для whitelist/blacklist через XHR/axios
if (this._urlObj && mustAggregateUrl(this._urlObj.toString(), this._method)) {
const base = new URL(this._urlObj.toString(), location.href);
base.searchParams.set('pageNo', '1');
base.searchParams.set('pageSize', String(PAGING.SIZE));
fetchAllPagesViaFetch(window.fetch.bind(window), base, {
credentials: 'include',
headers: this._headers || {}
}).then(({ template, list }) => {
const deduped = dedupList(list);
const payloadObj = { ...template, list: deduped, total: deduped.length };
const text = JSON.stringify(payloadObj);
defineRO(this, 'readyState', 4);
defineRO(this, 'status', 200);
defineRO(this, 'statusText', 'OK');
defineRO(this, 'responseURL', base.toString());
try {
this.getAllResponseHeaders = () => 'content-type: application/json; charset=utf-8\r\n';
this.getResponseHeader = (h) => (String(h).toLowerCase() === 'content-type' ? 'application/json; charset=utf-8' : null);
} catch {}
if (this.responseType === 'json') {
defineRO(this, 'response', payloadObj);
} else {
defineRO(this, 'response', text);
defineRO(this, 'responseText', text);
}
try { if (typeof this.onreadystatechange === 'function') this.onreadystatechange(new Event('readystatechange')); } catch {}
try { if (typeof this.onload === 'function') this.onload(new Event('load')); } catch {}
try {
this.dispatchEvent && this.dispatchEvent(new Event('readystatechange'));
this.dispatchEvent && this.dispatchEvent(new Event('load'));
this.dispatchEvent && this.dispatchEvent(new Event('loadend'));
} catch {}
}).catch(e => {
console.error('[Deeper Tools] XHR aggregate error:', e);
try { nativeSend.apply(this, arguments); } catch {}
});
return;
}
return nativeSend.apply(this, arguments);
};
// Перехват fetch (агрегатор + сканер доменов + сохранение пароля)
const originalFetch = window.fetch;
window.fetch = function (input, init) {
// Domain Scanner: подхват доменов
if (getScannerEnabled()) {
try {
const url = (typeof input === 'string') ? input : input.url;
const u = new URL(url, location.href);
addDomain(u.hostname);
} catch {}
}
// Захват логина/пароля из fetch /api/admin/login (JSON, URL-encoded, FormData)
try {
if (isAllowedHost()) {
const urlStr = typeof input === 'string' ? input : input.url;
const u = new URL(urlStr, location.href);
const method = (init && init.method ? String(init.method) : (typeof input !== 'string' && input?.method) || 'GET').toUpperCase();
if (u.pathname.startsWith('/api/admin/login') && method === 'POST' && init && init.body && !GM_getValue('adminPassword')) {
let pwd = null, uname = null;
if (typeof init.body === 'string') {
try {
if (init.body.trim().startsWith('{')) {
const p = JSON.parse(init.body);
pwd = p && p.password;
uname = p && (p.username || p.login || p.user);
} else {
const usp = new URLSearchParams(init.body);
pwd = usp.get('password');
uname = usp.get('username') || usp.get('login') || usp.get('user');
}
} catch {}
} else if (typeof init.body === 'object') {
try {
if (typeof init.body.get === 'function') {
pwd = init.body.get('password') || pwd; // FormData
uname = init.body.get('username') || init.body.get('login') || init.body.get('user') || uname;
}
} catch {}
}
if (uname) GM_setValue('adminUsername', uname);
if (pwd) {
GM_setValue('adminPassword', pwd);
console.log('[Deeper Tools] Пароль сохранён из fetch.');
}
}
}
} catch {}
// Агрегатор для whitelist/blacklist
try {
const urlStr = typeof input === 'string' ? input : input.url;
const method = init?.method || (typeof input !== 'string' && input?.method);
if (mustAggregateUrl(urlStr, method)) {
const base = new URL(urlStr, location.href);
base.searchParams.set('pageNo', '1');
base.searchParams.set('pageSize', String(PAGING.SIZE));
const headers = (init && init.headers) || {};
const fInit = { credentials: 'include', headers };
return fetchAllPagesViaFetch(originalFetch, base, fInit).then(({ template, list }) => {
const deduped = dedupList(list);
const payload = { ...template, list: deduped, total: deduped.length };
return new Response(JSON.stringify(payload), {
status: 200,
headers: { 'Content-Type': 'application/json; charset=utf-8' }
});
}).catch(e => {
console.error('[Deeper Tools] fetch aggregate error:', e);
return originalFetch.apply(this, arguments);
});
}
} catch {}
return originalFetch.apply(this, arguments);
};
// Отслеживание ресурсов, добавленных в DOM (сканер)
const resObserver = new MutationObserver(mutations => {
if (!getScannerEnabled()) return;
for (const m of mutations) {
if (!m.addedNodes) continue;
for (const node of m.addedNodes) {
if (node && node.tagName) {
const src = node.src || node.href;
if (src) {
try { addDomain(new URL(src, location.href).hostname); } catch {}
}
}
}
}
});
resObserver.observe(document.documentElement, { childList: true, subtree: true });
}
// По performance timeline (сканер)
setInterval(() => {
if (!getScannerEnabled()) return;
const entries = performance.getEntriesByType('resource') || [];
for (const entry of entries) {
try { addDomain(new URL(entry.name, location.href).hostname); } catch {}
}
}, 1000);
// Набор доменов и статус (сканер)
const domainSet = new Set();
const domainStatus = new Map();
function addDomain(domain) {
if (!domain) return;
if (!domainSet.has(domain)) {
domainSet.add(domain);
domainStatus.set(domain, 'testing');
testDomainAvailability(domain);
}
updateDomainList();
updateStats();
}
async function testDomainAvailability(domain) {
const schemes = location.protocol === 'https:' ? ['https:', 'http:'] : ['http:', 'https:'];
for (const scheme of schemes) {
const ok = await testWithScheme(domain, scheme).catch(() => false);
if (ok) { domainStatus.set(domain, 'ok'); updateDomainList(); updateStats(); return; }
}
domainStatus.set(domain, 'blocked');
updateDomainList(); updateStats();
}
function testWithScheme(domain, scheme) {
return new Promise((resolve) => {
const url = scheme + '//' + domain + '/favicon.ico';
GM_xmlhttpRequest({
method: 'GET',
url,
timeout: 7000,
onload: function (res) {
const st = res.status || 0;
resolve(st >= 200 && st < 400);
},
onerror: function () { resolve(false); },
ontimeout: function () { resolve(false); }
});
});
}
function ensureScannerContainer() {
if (!getScannerEnabled()) return;
if (document.getElementById('domain-scanner-container')) return;
const container = document.createElement('div');
container.id = 'domain-scanner-container';
Object.assign(container.style, {
position: 'fixed', top: '10px', right: '10px', width: '340px', maxHeight: '80vh',
overflowY: 'auto', backgroundColor: 'white', border: '1px solid black', zIndex: 10000,
padding: '10px', fontSize: '12px', fontFamily: 'monospace', color: 'black', whiteSpace: 'pre-wrap'
});
// Статистика
const statsBox = document.createElement('div');
statsBox.id = 'domain-stats';
statsBox.style.marginBottom = '8px';
function makeStatRow(labelId, labelText) {
const wrap = document.createElement('div');
wrap.style.marginBottom = '6px';
const label = document.createElement('div');
label.id = labelId + '-label';
label.textContent = labelText + ': 0 / 0';
label.style.marginBottom = '2px';
const bar = document.createElement('div');
bar.className = 'progress-outer';
Object.assign(bar.style, {
height: '6px', background: '#eee', border: '1px solid #bbb', borderRadius: '3px', overflow: 'hidden'
});
const inner = document.createElement('div');
inner.id = labelId + '-bar';
Object.assign(inner.style, {
height: '100%', width: '0%', background: labelId.includes('allowed') ? '#3cba54' : '#db3236'
});
bar.appendChild(inner);
wrap.appendChild(label);
wrap.appendChild(bar);
return wrap;
}
statsBox.appendChild(makeStatRow('allowed', 'Разрешённые'));
statsBox.appendChild(makeStatRow('blocked', 'Запрещённые'));
container.appendChild(statsBox);
// Список доменов
const domainList = document.createElement('div');
domainList.id = 'domain-list';
container.appendChild(domainList);
// Кнопка добавления в deeper
const addBtn = document.createElement('button');
addBtn.id = 'add-to-deeper-btn';
addBtn.textContent = 'Добавить в deeper';
Object.assign(addBtn.style, {
display: 'block', width: '100%', marginTop: '10px', padding: '6px 10px',
backgroundColor: '#f8f8f8', border: '1px solid #ccc', borderRadius: '4px', cursor: 'pointer', fontSize: '14px'
});
addBtn.addEventListener('click', addToDeeper);
container.appendChild(addBtn);
document.body.appendChild(container);
updateStats();
}
function updateDomainList() {
const container = document.getElementById('domain-scanner-container');
if (!container) return;
const listEl = container.querySelector('#domain-list');
const checked = {};
listEl.querySelectorAll('.domain-checkbox').forEach(cb => { checked[cb.dataset.domain] = cb.checked; });
const sortedArr = Array.from(domainSet).sort();
listEl.innerHTML = '';
sortedArr.forEach(domain => {
const row = document.createElement('div');
Object.assign(row.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '6px' });
const left = document.createElement('div');
left.style.display = 'flex';
left.style.alignItems = 'center';
left.style.gap = '6px';
const s = domainStatus.get(domain);
const icon = document.createElement('span');
icon.className = 'domain-status-icon';
icon.textContent = s === 'ok' ? '✅' : (s === 'blocked' ? '❌' : '⏳');
const t = document.createElement('span');
t.textContent = domain;
left.appendChild(icon);
left.appendChild(t);
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.classList.add('domain-checkbox');
cb.dataset.domain = domain;
cb.checked = !!checked[domain];
row.appendChild(left);
row.appendChild(cb);
listEl.appendChild(row);
});
}
function updateStats() {
const listRoot = document.getElementById('domain-scanner-container');
if (!listRoot) return;
let statsBox = document.getElementById('domain-stats');
if (!statsBox) {
statsBox = document.createElement('div');
statsBox.id = 'domain-stats';
statsBox.style.marginBottom = '8px';
function makeStatRow(labelId, labelText) {
const wrap = document.createElement('div');
wrap.style.marginBottom = '6px';
const label = document.createElement('div');
label.id = labelId + '-label';
label.textContent = labelText + ': 0 / 0';
label.style.marginBottom = '2px';
const bar = document.createElement('div');
bar.className = 'progress-outer';
Object.assign(bar.style, {
height: '6px', background: '#eee', border: '1px solid #bbb', borderRadius: '3px', overflow: 'hidden'
});
const inner = document.createElement('div');
inner.id = labelId + '-bar';
Object.assign(inner.style, {
height: '100%', width: '0%', background: labelId.includes('allowed') ? '#3cba54' : '#db3236'
});
bar.appendChild(inner);
wrap.appendChild(label);
wrap.appendChild(bar);
return wrap;
}
statsBox.appendChild(makeStatRow('allowed', 'Разрешённые'));
statsBox.appendChild(makeStatRow('blocked', 'Запрещённые'));
const domainList = document.getElementById('domain-list');
if (domainList && domainList.parentElement) {
domainList.parentElement.insertBefore(statsBox, domainList);
} else {
listRoot.appendChild(statsBox);
}
}
const total = domainSet.size || 0;
let ok = 0, blocked = 0;
for (const d of domainSet) {
const st = domainStatus.get(d);
if (st === 'ok') ok++;
else if (st === 'blocked') blocked++;
}
const allowedLabel = document.getElementById('allowed-label');
const blockedLabel = document.getElementById('blocked-label');
const allowedBar = document.getElementById('allowed-bar');
const blockedBar = document.getElementById('blocked-bar');
if (allowedLabel) allowedLabel.textContent = `Разрешённые: ${ok} / ${total}`;
if (blockedLabel) blockedLabel.textContent = `Запрещённые: ${blocked} / ${total}`;
const pctOk = total ? Math.round((ok / total) * 100) : 0;
const pctBlocked = total ? Math.round((blocked / total) * 100) : 0;
if (allowedBar) allowedBar.style.width = pctOk + '%';
if (blockedBar) blockedBar.style.width = pctBlocked + '%';
}
async function addToDeeper() {
try {
const response = await gmFetch('http://34.34.34.34/api/smartRoute/getRoutingWhitelist/domain?pageNo=1&pageSize=100');
if (response.status !== 200) { alert('[Deeper Tools] Ошибка при получении белого списка'); return; }
const data = await response.json();
const existingDomains = new Set();
const tunnelCodes = [];
if (Array.isArray(data.list)) {
for (const item of data.list) {
if (item.domainName) existingDomains.add(item.domainName);
if (item.tunnelCode) tunnelCodes.push(item.tunnelCode);
}
}
if (tunnelCodes.length === 0) tunnelCodes.push('defaultCode');
const container = document.getElementById('domain-scanner-container'); if (!container) return;
const checkboxes = container.querySelectorAll('.domain-checkbox');
const selected = []; checkboxes.forEach(cb => { if (cb.checked) selected.push(cb.dataset.domain); });
if (selected.length === 0) { alert('[Deeper Tools] Выберите домены для добавления.'); return; }
const newItems = [];
for (const d of selected) {
if (!existingDomains.has(d)) {
const randomIndex = Math.floor(Math.random() * tunnelCodes.length);
newItems.push({ domainName: d, tunnelCode: tunnelCodes[randomIndex] });
}
}
if (newItems.length === 0) { alert('[Deeper Tools] Нет новых доменов для добавления.'); return; }
for (const item of newItems) {
const r = await gmFetch('http://34.34.34.34/api/smartRoute/addToWhitelist/domain', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(item)
});
if (r.status !== 200) console.error('[Deeper Tools] Ошибка при добавлении домена:', item);
}
alert('[Deeper Tools] Новые домены добавлены в deeper!');
} catch (err) {
console.error('[Deeper Tools] Ошибка при добавлении в deeper:', err);
alert('Ошибка при добавлении. Смотрите консоль.');
}
}
if (isAllowedHost()) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bootDomainPageHelpers);
} else {
bootDomainPageHelpers();
}
new MutationObserver(bootDomainPageHelpers).observe(document.documentElement, { childList: true, subtree: true });
// Кнопки "Оптимизировать регионы" и "Тема"
window.addEventListener('DOMContentLoaded', () => {
const observer = new MutationObserver(() => {
const menu = document.querySelector('div[style*="flex-direction"]');
if (!menu) return;
observer.disconnect();
const buttonStyle = {
margin: '5px 0',
padding: '8px 14px',
backgroundColor: '#f8f8f8',
border: '1px solid #ccc',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
};
const optimizeBtn = document.createElement('button');
optimizeBtn.id = 'optimize-regions-btn';
optimizeBtn.textContent = 'Оптимизировать регионы';
Object.assign(optimizeBtn.style, buttonStyle);
optimizeBtn.addEventListener('click', optimizeRegions);
menu.appendChild(optimizeBtn);
const themeBtn = document.createElement('button');
themeBtn.id = 'toggle-theme-btn';
themeBtn.textContent = 'Тема';
Object.assign(themeBtn.style, buttonStyle);
themeBtn.addEventListener('click', () => {
currentThemeIndex = (currentThemeIndex + 1) % themeNames.length;
applyTheme(currentThemeIndex);
});
menu.appendChild(themeBtn);
});
observer.observe(document.body, { childList: true, subtree: true });
});
// Плавающая иконка-меню
const iconButton = document.createElement('div');
Object.assign(iconButton.style, {
position: 'fixed', width: '25px', height: '25px', top: '10px', right: '10px', zIndex: '9999',
backgroundColor: 'rgb(240, 240, 252)', borderRadius: '4px', boxShadow: '0 2px 5px rgba(0,0,0,0.3)',
cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center'
});
const img = document.createElement('img');
img.src = 'https://avatars.mds.yandex.net/get-socsnippets/10235467/2a0000019509580bc84108597cea65bc46ee/square_83';
img.style.maxWidth = '80%'; img.style.maxHeight = '80%'; iconButton.appendChild(img);
const menuContainer = document.createElement('div');
Object.assign(menuContainer.style, {
position: 'fixed', top: '45px', right: '10px', zIndex: '10000', padding: '10px',
border: '1px solid #ccc', borderRadius: '4px', boxShadow: '0 2px 5px rgba(0,0,0,0.3)',
backgroundColor: '#fff', display: 'none', flexDirection: 'column'
});
function toggleMenu() { menuContainer.style.display = (menuContainer.style.display === 'none' ? 'flex' : 'none'); }
iconButton.addEventListener('click', toggleMenu);
const buttonStyle2 = {
margin: '5px 0', padding: '8px 14px', backgroundColor: '#f8f8f8',
border: '1px solid #ccc', borderRadius: '4px', cursor: 'pointer', fontSize: '14px'
};
const forgetBtn = document.createElement('button'); forgetBtn.textContent = 'Забыть пароль'; Object.assign(forgetBtn.style, buttonStyle2);
const allToffBtn = document.createElement('button'); allToffBtn.textContent = 'All_T_OFF'; allToffBtn.title = 'Отключить все домены у выбранных туннелей и переключить их на другой.'; Object.assign(allToffBtn.style, buttonStyle2);
menuContainer.append(forgetBtn, allToffBtn);
function ensureMenu() {
if (!document.body.contains(iconButton)) document.body.appendChild(iconButton);
if (!document.body.contains(menuContainer)) document.body.appendChild(menuContainer);
}
document.addEventListener('DOMContentLoaded', ensureMenu);
new MutationObserver(ensureMenu).observe(document.documentElement, { childList: true, subtree: true });
// Забыть пароль (и логин)
forgetBtn.addEventListener('click', () => {
if (confirm('Внимание! Логин и пароль будут очищены. Продолжить?')) {
GM_setValue('adminPassword', null);
GM_setValue('adminUsername', null);
alert('[Deeper Tools] Данные очищены. Авторизуйтесь вручную.');
}
});
allToffBtn.addEventListener('click', showAllToffPopup);
async function optimizeRegions() {
if (!isAllowedHost()) return;
console.log('🔄 Запуск оптимизации регионов (батчи по 5)');
const btn = document.getElementById('optimize-regions-btn');
if (btn) { btn.disabled = true; btn.textContent = 'Оптимизация…'; }
const regionMap = {
AMN: ["BM","CA","GL","MX","PM","US","UB","UC","UD","UE","UF"],
AMC: ["AG","AI","AW","BB","BL","BQ","BS","CU","CW","DM","DO","GD...N","KY","LC","MF","MQ","MS","PR","SX","TC","TT","VC","VG","VI"],
AMM: ["BZ","CR","GT","HN","NI","PA","SV"],
AMS: ["AR","BO","BR","CL","CO","EC","FK","GF","GS","GY","PE","PY","SR","UY","VE"],
ASC: ["KG","KZ","TJ","TM","UZ"],
ASE: ["CN","HK","JP","KP","KR","MN","MO","TW"],
ASW: ["AE","AM","AZ","BH","IR","GE","IL","IQ","JO","KW","LB","OM","PS","QA","SA","SY","YE"],
ASS: ["AF","BD","BT","IN","LK","MV","NP","PK"],
ASD: ["BN","ID","KH","LA","MM","MY","PH","SG","TH","TL","VN"],
AFN: ["DZ","EG","LY","MA","SD","TN"],
AFS: ["BW","LS","NA","SZ","ZA"],
AFE: ["BI","DJ","ER","ET","KE","KM","MG","MU","MW","MZ","RE","RW","SC","SO","TZ","UG","YT","ZM","ZW"],
AFW: ["AO","BF","BJ","BV","CI","CV","GH","GM","GN","GW","LR","ML","MR","NE","NG","SH","SL","SN","ST","TG"],
EUN: ["GG","IE","IM","JE","UK"],
EEU: ["AT","BE","CH","DE","DK","FI","FO","FR","IS","IT","LI","LU","MC","NL","NO","SE","SJ","SM","VA"],
EUS: ["AD","AL","BA","BG","BY","CZ","EE","ES","GI","GR","HR","HU","LT","LV","MD","ME","MK","PL","PT","RO","RS","RU","SI","SK","UA"],
OCN: ["AU","NF","NZ"],
OCS: ["AS","CK","FJ","FM","GU","KI","MH","MP","NC","NR","NU","PF","PG","PN","PW","SB","TK","TO","TV","UM","VU","WF","WS"]
};
async function listWhitelist(pageNo, pageSize) {
const res = await gmFetch(`${location.origin}/api/smartRoute/getRoutingWhitelist/domain?pageNo=${pageNo}&pageSize=${pageSize}`);
return res.json();
}
async function deleteFromWhitelist(domains) {
return gmFetch(`${location.origin}/api/smartRoute/deleteFromWhitelist/domain`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(domains)
});
}
async function processRegion(regionCode) {
console.log(`➡️ Обработка региона ${regionCode}`);
const countries = regionMap[regionCode];
let page = 1, pageSize = 100, done = false;
const accumulated = [];
while (!done) {
const data = await listWhitelist(page, pageSize);
if (!data || !Array.isArray(data.list)) break;
for (const item of data.list) {
if (countries.includes(item.countryCode)) accumulated.push(item.domainName);
}
if (data.list.length < pageSize) done = true; else page++;
}
if (accumulated.length === 0) {
console.log(`⏭ Регион ${regionCode}: нет доменов для удаления`);
return;
}
console.log(`🗑 Регион ${regionCode}: удаляем ${accумulated.length} доменов`);
await deleteFromWhitelist(accumulated);
}
const codeList = Object.keys(regionMap);
const batchSize = 5;
for (let i = 0; i < codeList.length; i += batchSize) {
const batch = codeList.slice(i, i + batchSize);
console.log('⚙️ Батч:', batch.join(', '));
await Promise.all(batch.map(rc => processRegion(rc)));
}
if (btn) { btn.disabled = false; btn.textContent = 'Оптимизировать регионы'; }
console.log('✅ Оптимизация завершена');
}
async function showAllToffPopup() {
const overlay = document.createElement('div');
Object.assign(overlay.style, {
position: 'fixed', inset: 0, background: 'rgba(0,0,0,.5)', zIndex: 20000
});
const popup = document.createElement('div');
Object.assign(popup.style, {
maxWidth: '600px', width: '95%', position: 'fixed', top: '50%', left: '50%',
transform: 'translate(-50%,-50%)', background: '#fff', padding: '20px',
borderRadius: '8px', boxShadow: '0 2px 10px rgba(0,0,0,.3)', color: '#000'
});
const title = document.createElement('h3');
title.textContent = 'Массовое отключение доменов';
popup.appendChild(title);
const tunnelsContainer = document.createElement('div');
Object.assign(tunnelsContainer.style, {
maxHeight: '300px', overflowY: 'auto', marginBottom: '10px'
});
popup.appendChild(tunnelsContainer);
const btns = document.createElement('div');
Object.assign(btns.style, { display: 'flex', justifyContent: 'flex-end', gap: '10px' });
const switchAllBtn = document.createElement('button');
switchAllBtn.textContent = 'Переключить все';
Object.assign(switchAllBtn.style, { background: '#0077cc', color: '#fff', borderRadius: '4px', padding: '8px 14px', cursor: 'pointer' });
const offBtn = document.createElement('button');
offBtn.textContent = 'Отключиться';
Object.assign(offBtn.style, { background: '#bb0000', color: '#fff', borderRadius: '4px', padding: '8px 14px', cursor: 'pointer' });
const randomizeBtn = document.createElement('button');
randomizeBtn.textContent = 'Рандомайзер';
Object.assign(randomizeBtn.style, { background: '#007700', color: '#fff', borderRadius: '4px', padding: '8px 14px', cursor: 'pointer' });
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Отмена';
Object.assign(cancelBtn.style, { background: '#666', color: '#fff', borderRadius: '4px', padding: '8px 14px', cursor: 'pointer' });
btns.append(switchAllBtn, offBtn, randomizeBtn, cancelBtn);
popup.appendChild(btns);
overlay.appendChild(popup);
document.body.appendChild(overlay);
function closePopup() { overlay.remove(); }
cancelBtn.addEventListener('click', closePopup);
// helpers
async function listTunnels() {
const r = await gmFetch(`${location.origin}/api/smartRoute/listTunnels`);
if (r.status !== 200) throw new Error('listTunnels failed');
return r.json();
}
async function getAllWhitelist() {
const pageSize = 100;
let pageNo = 1, done = false, all = [];
while (!done) {
const r = await gmFetch(`${location.origin}/api/smartRoute/getRoutingWhitelist/domain?pageNo=${pageNo}&pageSize=${pageSize}`);
const d = await r.json();
if (!d || !Array.isArray(d.list)) break;
all.push(...d.list);
if (d.list.length < pageSize) done = true; else pageNo++;
}
return all;
}
const pickBest = (cands) => {
if (!cands.length) return null;
const maxActive = Math.max(...cands.map(t => Number(t.activeNum || 0)));
const best = cands.filter(t => Number(t.activeNum || 0) === maxActive);
return best[Math.floor(Math.random() * best.length)];
};
// render tunnels
let tunnelsList = [];
try {
tunnelsList = await listTunnels();
} catch (e) {
console.error('[Deeper Tools] Ошибка при получении туннелей:', e);
alert('Ошибка получения списка туннелей. Смотрите консоль.');
return closePopup();
}
tunnelsList.forEach(t => {
const row = document.createElement('div');
Object.assign(row.style, { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '5px', fontSize: '14px' });
const left = document.createElement('div');
left.style.display = 'flex';
left.style.alignItems = 'center';
const text = document.createElement('span');
text.textContent = `${t.countryCode} ${t.regionCode}`;
left.appendChild(text);
const right = document.createElement('div');
Object.assign(right.style, { display: 'flex', alignItems: 'center' });
const active = document.createElement('span');
Object.assign(active.style, { width: '30px', textAlign: 'right', display: 'inline-block', marginRight: '10px' });
active.textContent = t.activeNum;
const chk = document.createElement('input');
chk.type = 'checkbox';
chk.dataset.tunnelCode = t.tunnelCode;
chk.dataset.regionCode = t.regionCode;
chk.dataset.countryCode = t.countryCode;
chk.dataset.activeNum = t.activeNum;
right.append(active, chk);
row.append(left, right);
tunnelsContainer.appendChild(row);
});
switchAllBtn.addEventListener('click', async () => {
const checked = [...tunnelsContainer.querySelectorAll('input[type=checkbox]')].filter(ch => ch.checked);
if (!checked.length) return alert('Не выбрано ни одного туннеля.');
try {
const whitelist = await getAllWhitelist();
const fresh = await listTunnels();
const selectedCodes = checked.map(ch => ch.dataset.tunnelCode);
for (const entry of whitelist) {
const cands = fresh.filter(t => selectedCodes.includes(t.tunnelCode));
const chosen = pickBest(cands);
if (!chosen) continue;
try {
await gmFetch(`${location.origin}/api/smartRoute/editWhiteEntry/domain`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domainName: entry.domainName, fromTunnelCode: entry.tunnelCode, toTunnelCode: chosen.tunnelCode })
});
} catch (e) { console.error('[Deeper Tools] Ошибка переключения для домена:', entry.domainName, e); }
}
alert('Массовое переключение выполнено.');
closePopup();
} catch (e) {
console.error('[Deeper Tools] Ошибка "Переключить все":', e);
alert('Ошибка при переключении. Смотрите консоль.');
}
});
offBtn.addEventListener('click', async () => {
const checked = [...tunnelsContainer.querySelectorAll('input[type=checkbox]')].filter(ch => ch.checked);
if (!checked.length) return alert('Не выбрано ни одного туннеля.');
try {
const fresh = await listTunnels();
for (const item of checked) {
const fromCode = item.dataset.tunnelCode;
const wl = await getAllWhitelist();
const entries = wl.filter(e => e.tunnelCode === fromCode);
const cands = fresh.filter(t => t.tunnelCode !== fromCode);
const chosenBase = pickBest(cands);
if (!chosenBase) continue;
for (const entry of entries) {
const chosen = pickBest(cands) || chosenBase;
try {
await gmFetch(`${location.origin}/api/smartRoute/editWhiteEntry/domain`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domainName: entry.domainName, fromTunnelCode: fromCode, toTunnelCode: chosen.tunnelCode })
});
} catch (e) { console.error('[Deeper Tools] Ошибка отключения для домена:', entry.domainName, e); }
}
}
alert('Массовое отключение выполнено.');
closePopup();
} catch (e) {
console.error('[Deeper Tools] Ошибка "Отключиться":', e);
alert('Ошибка при отключении. Смотрите консоль.');
}
});
randomizeBtn.addEventListener('click', async () => {
try {
const wl = await getAllWhitelist();
if (!wl.length) return alert('Нет доменов для распределения.');
const allTunnels = (await listTunnels()).map(t => t.tunnelCode);
if (!allTunnels.length) return alert('Нет доступных туннелей.');
const domains = wl.slice();
for (let i = domains.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[domains[i], domains[j]] = [domains[j], domains[i]];
}
for (let i = 0; i < domains.length; i++) {
const entry = domains[i];
const toTunnel = allTunnels[i % allTunnels.length];
await gmFetch(`${location.origin}/api/smartRoute/editWhiteEntry/domain`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domainName: entry.domainName, fromTunnelCode: entry.tunnelCode, toTunnelCode: toTunnel })
});
}
alert('Рандомизация доменов завершена!');
closePopup();
} catch (e) {
console.error('[Deeper Tools] Ошибка рандомизации:', e);
alert('Ошибка при рандомизации. Смотрите консоль.');
}
});
}
}
/* ---------------------------------
* Меню Tampermonkey: Domain Scanner
* --------------------------------- */
let scannerMenuCommandId = null;
function updateScannerMenuCommand() {
if (scannerMenuCommandId && typeof GM_unregisterMenuCommand === 'function') {
GM_unregisterMenuCommand(scannerMenuCommandId);
}
if (typeof GM_registerMenuCommand === 'function') {
const currentState = getScannerEnabled();
const label = 'Domain Scanner: ' + (currentState ? '🟢' : '🔴');
scannerMenuCommandId = GM_registerMenuCommand(label, () => setScannerEnabled(!getScannerEnabled()));
}
}
if (GM_getValue('domainScannerEnabled') === undefined) GM_setValue('domainScannerEnabled', false);
updateScannerMenuCommand();
if (getScannerEnabled()) {
if (['complete', 'interactive'].includes(document.readyState)) ensureScannerContainer();
else document.addEventListener('DOMContentLoaded', ensureScannerContainer);
}
})();