function cidrToMask(bits) { const n = parseInt(bits || "32", 10); if (n === 32) return "255.255.255.255"; return Array.from({ length: 4 }, (_, i) => (65280 >> Math.min(8, Math.max(0, n - i * 8))) & 255).join("."); } function parseDomainsColumn(text) { if (!text) return []; return text.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("Все домены")); } function parseIpsColumn(text) { if (!text) return []; const lines = text.split("\n"); const ips = []; for (const raw of lines) { const line = raw.trim(); if (!line || line.startsWith("Все IP") || line.startsWith("IPv6")) break; const cidr = line.replace(/\s*\(диапазон\)\s*$/, ""); if (/^\d+\.\d+\.\d+\.\d+(\/\d+)?$/.test(cidr)) ips.push(cidr); } return ips; } function buildBatFromIps(ips) { const lines = ["@echo off", "rem Keenetic — выбранные IP из поиска", ""]; const seen = new Set(); ips.forEach((entry) => { if (entry.includes("/")) { const [ip, bits] = entry.split("/"); if (!ip || seen.has(entry)) return; seen.add(entry); lines.push(`route ADD ${ip} MASK ${cidrToMask(bits)} 0.0.0.0`); } else if (/^\d+\.\d+\.\d+\.\d+$/.test(entry) && !seen.has(entry)) { seen.add(entry); lines.push(`route ADD ${entry} MASK 255.255.0.0 0.0.0.0`); } }); if (lines.length <= 3) lines.push("rem Нет IP для выбранных записей"); return lines.join("\r\n") + "\r\n"; } function filterBatContent(batContent, selectedIps) { if (!batContent || !selectedIps.size) return ""; const selected = new Set([...selectedIps].map((ip) => ip.split("/")[0])); return batContent.split("\n").filter((line) => { const m = line.match(/route add (\S+)/i); return m && selected.has(m[1]); }).join("\n") + (selected.size ? "\n" : ""); } function CopyBlock({ title, text, filename }) { const [copied, setCopied] = React.useState(false); const copy = async () => { try { await navigator.clipboard.writeText(text || ""); setCopied(true); setTimeout(() => setCopied(false), 1500); } catch (_) {} }; const download = () => { const blob = new Blob([text || ""], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename || "export.txt"; a.click(); URL.revokeObjectURL(url); }; if (!text) return null; return (
{title}
{text}
); } function SelectableChips({ items, selected, onToggle, label }) { if (!items.length) return null; return (
{label} — клик для выбора ({selected.size}/{items.length})
{items.map((item) => { const on = selected.has(item); return ( ); })}
); } const btnStyle = { border: "1px solid var(--line)", background: "#fff", borderRadius: 10, padding: "8px 12px", cursor: "pointer", fontSize: 13, }; function SearchApp() { const [query, setQuery] = React.useState(""); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(""); const [result, setResult] = React.useState(null); const [selectedDomains, setSelectedDomains] = React.useState(new Set()); const [selectedIps, setSelectedIps] = React.useState(new Set()); const resultDomains = React.useMemo(() => parseDomainsColumn(result?.domains_column), [result]); const resultIps = React.useMemo(() => parseIpsColumn(result?.ips_column), [result]); const toggleDomain = (d) => { setSelectedDomains((prev) => { const next = new Set(prev); if (next.has(d)) next.delete(d); else next.add(d); return next; }); }; const toggleIp = (ip) => { setSelectedIps((prev) => { const next = new Set(prev); if (next.has(ip)) next.delete(ip); else next.add(ip); return next; }); }; const runSearch = async (e) => { e.preventDefault(); setError(""); setResult(null); setSelectedDomains(new Set()); setSelectedIps(new Set()); const trimmed = query.trim(); if (!trimmed) return; setLoading(true); try { const isMulti = /[\n,]/.test(trimmed) || trimmed.split(/\s+/).length > 1; const body = isMulti ? { domains: trimmed.split(/[\s,\n]+/).filter(Boolean) } : { domain: trimmed }; const res = await fetch("/api/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.detail || `HTTP ${res.status}`); setResult(data); setSelectedDomains(new Set(parseDomainsColumn(data.domains_column))); setSelectedIps(new Set(parseIpsColumn(data.ips_column))); } catch (err) { setError(err.message || "Ошибка поиска"); } finally { setLoading(false); } }; const downloadBat = (content, filename) => { if (!content) return; const blob = new Blob([content], { type: "application/octet-stream" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename || "routes.bat"; a.click(); URL.revokeObjectURL(url); }; const downloadSelectedBat = () => { let bat = filterBatContent(result?.bat_content, selectedIps); if (!bat.trim()) bat = buildBatFromIps([...selectedIps]); downloadBat(bat, (result?.domains?.[0] || "selected") + "-selected.bat"); }; const copySelectedDomains = async () => { try { await navigator.clipboard.writeText([...selectedDomains].join("\n")); } catch (_) {} }; return (

Поиск доменов

CT, DNS, reverse IP, geosite/geoip — как в Telegram-боте. Один домен или пакет через пробел/запятую/новую строку.