Spaces:
Running
Running
| <html lang="ru"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Advanced Leak-based Username Generator — willhaben style</title> | |
| <style> | |
| :root{ | |
| --bg:#0b0f14;--panel:#0f1720;--accent:#39a6ff;--muted:#9fb0c8;--text:#e6f1fb; | |
| --ok:#5ee07a;--warn:#ffcc33;--err:#ff6b6b; | |
| } | |
| *{box-sizing:border-box} | |
| body{margin:0;font-family:Inter, Roboto, Arial, sans-serif;background:linear-gradient(180deg,#061018,#071820);color:var(--text);min-height:100vh;display:flex;flex-direction:column} | |
| header{padding:20px 22px;border-bottom:1px solid rgba(255,255,255,0.03)} | |
| h1{margin:0;font-size:18px} | |
| main{display:grid;grid-template-columns:380px 1fr;gap:16px;padding:18px;align-items:start} | |
| @media (max-width:980px){main{grid-template-columns:1fr;padding:12px}} | |
| .card{background:linear-gradient(180deg,var(--panel),#0b1118);border-radius:12px;padding:14px;border:1px solid rgba(255,255,255,0.03);box-shadow:0 8px 30px rgba(0,0,0,0.6)} | |
| label{display:block;font-size:12px;color:var(--muted);margin-bottom:6px} | |
| input[type="file"]{color:var(--text)} | |
| .row{display:flex;gap:8px;align-items:center} | |
| input[type="text"], input[type="number"], select, textarea { | |
| width:100%;padding:10px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:#07121a;color:var(--text); | |
| } | |
| textarea{min-height:120px;resize:vertical;font-family:ui-monospace,Menlo,Consolas,monospace} | |
| button{background:linear-gradient(90deg,var(--accent),#6ad6ff);border:none;color:#04202a;padding:10px 12px;border-radius:8px;font-weight:700;cursor:pointer} | |
| button.ghost{background:transparent;border:1px solid rgba(255,255,255,0.04);color:var(--text)} | |
| .muted{color:var(--muted);font-size:13px} | |
| .small{font-size:12px;color:var(--muted)} | |
| .stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:8px} | |
| .stat{background:rgba(255,255,255,0.02);padding:10px;border-radius:8px;text-align:center} | |
| .stat b{display:block;font-size:18px;color:var(--text)} | |
| table{width:100%;border-collapse:collapse;font-size:13px;margin-top:8px} | |
| th,td{padding:6px 8px;border-bottom:1px dashed rgba(255,255,255,0.03);text-align:left} | |
| th{color:var(--muted);font-weight:600;font-size:12px} | |
| .controls{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px} | |
| .progress{height:10px;background:rgba(255,255,255,0.03);border-radius:8px;overflow:hidden} | |
| .progress > i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),#6ad6ff);width:0%} | |
| .footer{padding:16px;color:var(--muted);font-size:13px;text-align:center} | |
| .tag{display:inline-block;padding:6px 8px;background:rgba(255,255,255,0.03);border-radius:999px;font-size:12px;margin:4px 4px 0 0} | |
| .codebox{background:#041219;border:1px solid rgba(255,255,255,0.02);padding:10px;border-radius:8px;font-family:ui-monospace,Menlo,Consolas,monospace;color:#cfeefb;font-size:13px;max-height:260px;overflow:auto} | |
| .flex{display:flex;gap:8px;align-items:center} | |
| .right-actions{display:flex;gap:8px;align-items:center;justify-content:flex-end} | |
| .switch-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin: 8px 0; | |
| } | |
| .switch { | |
| position: relative; | |
| display: inline-block; | |
| width: 40px; | |
| height: 20px; | |
| } | |
| .switch input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .slider { | |
| position: absolute; | |
| cursor: pointer; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: #333; | |
| transition: .4s; | |
| border-radius: 34px; | |
| } | |
| .slider:before { | |
| position: absolute; | |
| content: ""; | |
| height: 16px; | |
| width: 16px; | |
| left: 2px; | |
| bottom: 2px; | |
| background-color: white; | |
| transition: .4s; | |
| border-radius: 50%; | |
| } | |
| input:checked + .slider { | |
| background-color: var(--accent); | |
| } | |
| input:checked + .slider:before { | |
| transform: translateX(20px); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>Advanced leak-based username generator — robust & streaming</h1> | |
| <div class="small">Загрузите очищенный список логинов (txt). Анализируем паттерны и генерируем правдоподобные варианты по распределению.</div> | |
| </header> | |
| <main> | |
| <!-- LEFT: Input / Analysis controls --> | |
| <div class="card"> | |
| <label>1) Загрузить файл с логинами (txt)</label> | |
| <input id="fileInput" type="file" accept=".txt" /> | |
| <div class="small" style="margin-top:8px">Поддерживается чтение больших файлов через поток (если браузер поддерживает), иначе — FileReader. Формат: один email на строку.</div> | |
| <div style="margin-top:12px"> | |
| <label>2) Настройки анализа</label> | |
| <div class="small">Автоматическое определение паттернов, доменов, имен, фамилий и суффиксов. Можно задать свои значения или использовать автоматически определенные.</div> | |
| <div class="switch-container"> | |
| <label class="small">Автоопределение имен/фамилий</label> | |
| <label class="switch"> | |
| <input type="checkbox" id="autoDetectNames" checked> | |
| <span class="slider"></span> | |
| </label> | |
| </div> | |
| <div style="margin-top:8px" class="row"> | |
| <input id="sampleNames" type="text" placeholder="Примеры имён через запятую (опц.)" /> | |
| <input id="sampleLastnames" type="text" placeholder="Примеры фамилий (опц.)" /> | |
| </div> | |
| <div class="switch-container" style="margin-top:8px"> | |
| <label class="small">Автоопределение суффиксов</label> | |
| <label class="switch"> | |
| <input type="checkbox" id="autoDetectSuffixes" checked> | |
| <span class="slider"></span> | |
| </label> | |
| </div> | |
| <div style="margin-top:8px" class="row"> | |
| <label style="margin:0">Доп. суффиксы (через запятую)</label> | |
| </div> | |
| <input id="manualSuffixes" type="text" placeholder="123,007,1984,2005" /> | |
| <div class="switch-container" style="margin-top:8px"> | |
| <label class="small">Использовать ручной домен</label> | |
| <label class="switch"> | |
| <input type="checkbox" id="useManualDomain"> | |
| <span class="slider"></span> | |
| </label> | |
| </div> | |
| <input id="manualDomain" type="text" placeholder="Введите домен вручную (например: example.com)" style="margin-top:8px; display: none;" /> | |
| </div> | |
| <div style="margin-top:12px"> | |
| <label>3) Анализировать / Сброс</label> | |
| <div class="controls"> | |
| <button id="analyzeBtn">Проанализировать файл</button> | |
| <button id="resetBtn" class="ghost">Сбросить состояние</button> | |
| </div> | |
| </div> | |
| <div class="stats" style="margin-top:12px"> | |
| <div class="stat"> | |
| <div class="muted">Строк в файле</div> | |
| <b id="statLines">0</b> | |
| </div> | |
| <div class="stat"> | |
| <div class="muted">Уникальных локал-партов</div> | |
| <b id="statUnique">0</b> | |
| </div> | |
| <div class="stat"> | |
| <div class="muted">Доменов</div> | |
| <b id="statDomains">0</b> | |
| </div> | |
| </div> | |
| <div style="margin-top:12px"> | |
| <label>4) Результаты анализа (топ паттернов & домены)</label> | |
| <div style="max-height:260px;overflow:auto"> | |
| <table id="patternsTable"> | |
| <thead><tr><th>Паттерн</th><th>Частота</th><th>Примеры</th></tr></thead> | |
| <tbody></tbody> | |
| </table> | |
| <table id="domainsTable" style="margin-top:10px"> | |
| <thead><tr><th>Домен</th><th>Частота</th></tr></thead> | |
| <tbody></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div style="margin-top:12px"> | |
| <label>5) Настройки генерации</label> | |
| <div class="small">Укажите количество генерируемых вариантов и опции экспорта.</div> | |
| <div class="row" style="margin-top:6px"> | |
| <input id="genCount" type="number" min="1" value="500" /> | |
| <select id="exportFormat" title="Формат экспорта"> | |
| <option value="txt">.txt (один в строке)</option> | |
| <option value="csv">.csv (localpart,domain,pattern)</option> | |
| <option value="json">.json (массив объектов)</option> | |
| </select> | |
| </div> | |
| <div style="margin-top:8px" class="row"> | |
| <label style="margin:0"><input id="dedupe" type="checkbox" checked /> Удалять дубликаты (может потребовать памяти)</label> | |
| </div> | |
| <div style="margin-top:8px" class="row"> | |
| <label style="margin:0"><input id="preferLocalDomains" type="checkbox" checked/> Предпочитать локальные домены (gmx.at, aon.at, liwest.at ...)</label> | |
| </div> | |
| <div class="controls" style="margin-top:10px"> | |
| <button id="generateBtn">Сгенерировать</button> | |
| <button id="previewBtn" class="ghost">Показать примеры</button> | |
| </div> | |
| <div style="margin-top:10px"> | |
| <div class="small">Прогресс:</div> | |
| <div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100"> | |
| <i id="progressBar"></i> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- RIGHT: Output / Preview / Export --> | |
| <div class="card"> | |
| <div style="display:flex;justify-content:space-between;align-items:center"> | |
| <div> | |
| <label>Выход / предпросмотр</label> | |
| <div class="small">Вы можете просмотреть выборку, скачать полный результат или скопировать часть.</div> | |
| </div> | |
| <div class="right-actions"> | |
| <button id="copyPreview" class="ghost">Копировать видимые</button> | |
| <button id="downloadBtn" class="ghost" disabled>Скачать результат</button> | |
| </div> | |
| </div> | |
| <div style="margin-top:8px" class="codebox" id="previewBox" aria-live="polite"></div> | |
| <div style="margin-top:12px" class="flex"> | |
| <div style="flex:1"> | |
| <label class="small">Фильтр предпросмотра</label> | |
| <input id="previewFilter" type="text" placeholder="например: .gmx.at или 2005 или alex" /> | |
| </div> | |
| <div style="width:160px"> | |
| <label class="small">Показать N примеров</label> | |
| <input id="previewLimit" type="number" min="1" value="50" /> | |
| </div> | |
| </div> | |
| <div style="margin-top:10px" class="small">Экспорт может быть большим — для больших объёмов генерируем файл как Blob и даём ссылку для скачивания.</div> | |
| <div style="margin-top:12px"> | |
| <label class="small">Лог действий</label> | |
| <div id="logBox" class="codebox" style="max-height:160px"></div> | |
| </div> | |
| </div> | |
| </main> | |
| <div class="footer card"> | |
| <div class="small">Инструмент анализирует паттерны локальной части email (localpart) — firstname.lastname, initials, nick+digits, pure nicks и т.д. — и строит распределение для генерации. Процесс оптимизирован для больших файлов (поточное чтение, батчи и асинхронная генерация).</div> | |
| </div> | |
| <script> | |
| (() => { | |
| const el = id => document.getElementById(id); | |
| const log = (t) => { | |
| const b = el('logBox'); | |
| const time = new Date().toISOString().replace('T',' ').split('.')[0]; | |
| b.textContent = `${time} — ${t}\n` + b.textContent; | |
| }; | |
| function safeLower(s){ return (s||'').trim().toLowerCase(); } | |
| function uniqArray(arr){ return [...new Set(arr)]; } | |
| function sum(obj){ | |
| return Object.values(obj).reduce((a,b)=>a+(b||0),0); | |
| } | |
| function pickWeighted(map){ | |
| const entries = Object.entries(map); | |
| const total = entries.reduce((a,[,w])=>a+w,0); | |
| if (total === 0) return entries.length ? entries[0][0] : null; | |
| let r = Math.random()*total, acc=0; | |
| for(const [k,w] of entries){ acc+=w; if(r < acc) return k; } | |
| return entries[0][0]; | |
| } | |
| const PATTERNS = { | |
| 'fn.dot.ln': /^[a-zäöüß]+[.][a-zäöüß]+[0-9]*$/i, | |
| 'fi.dot.ln': /^[a-zäöüß][.][a-zäöüß]+[0-9]*$/i, | |
| 'fnln': /^[a-zäöüß]+[a-zäöüß]+[0-9]*$/i, | |
| 'nick_digits': /^[a-z0-9._-]*[0-9]{2,6}$/i, | |
| 'nick': /^[a-z][a-z0-9._-]{1,20}$/i, | |
| 'initials': /^[a-z]{2,4}$/i, | |
| 'contains_special': /[^a-z0-9._-]/i | |
| }; | |
| let state = { | |
| totalLines: 0, | |
| uniqueLocalparts: new Set(), | |
| domainCounts: Object.create(null), | |
| patternCounts: Object.create(null), | |
| patternExamples: Object.create(null), | |
| sampleLocalparts: [], | |
| domainsList: [], | |
| manualSuffixes: [], | |
| detectedFirstNames: new Set(), | |
| detectedLastNames: new Set(), | |
| detectedSuffixes: new Set(), | |
| analyzed: false | |
| }; | |
| Object.keys(PATTERNS).forEach(k => { state.patternCounts[k]=0; state.patternExamples[k]=new Set(); }); | |
| el('useManualDomain').addEventListener('change', function() { | |
| el('manualDomain').style.display = this.checked ? 'block' : 'none'; | |
| }); | |
| async function processFile(file) { | |
| state.totalLines = 0; | |
| state.uniqueLocalparts = new Set(); | |
| state.domainCounts = {}; | |
| state.detectedFirstNames = new Set(); | |
| state.detectedLastNames = new Set(); | |
| state.detectedSuffixes = new Set(); | |
| Object.keys(state.patternCounts).forEach(k => state.patternCounts[k]=0); | |
| Object.keys(state.patternExamples).forEach(k => state.patternExamples[k].clear()); | |
| state.sampleLocalparts = []; | |
| el('statLines').textContent = '0'; | |
| el('statUnique').textContent = '0'; | |
| el('statDomains').textContent = '0'; | |
| el('patternsTable').querySelector('tbody').innerHTML = ''; | |
| el('domainsTable').querySelector('tbody').innerHTML = ''; | |
| el('previewBox').textContent = ''; | |
| el('logBox').textContent = ''; | |
| log(`Начало анализа файла: ${file.name}`); | |
| if (file.stream && typeof file.stream === 'function') { | |
| log('Использую File.stream() для по-частного чтения (stream).'); | |
| await streamFileProcessing(file); | |
| } else { | |
| log('File.stream() не поддерживается — использую FileReader (fallback).'); | |
| await fileReaderProcessing(file); | |
| } | |
| state.domainsList = Object.keys(state.domainCounts).sort((a,b)=>state.domainCounts[b]-state.domainCounts[a]); | |
| el('statLines').textContent = String(state.totalLines); | |
| el('statUnique').textContent = String(state.uniqueLocalparts.size); | |
| el('statDomains').textContent = String(Object.keys(state.domainCounts).length); | |
| if (el('autoDetectNames').checked) { | |
| const firstNames = Array.from(state.detectedFirstNames).slice(0, 20); | |
| const lastNames = Array.from(state.detectedLastNames).slice(0, 20); | |
| if (firstNames.length > 0 && !el('sampleNames').value) { | |
| el('sampleNames').value = firstNames.join(','); | |
| log(`Автоопределено ${firstNames.length} имен`); | |
| } | |
| if (lastNames.length > 0 && !el('sampleLastnames').value) { | |
| el('sampleLastnames').value = lastNames.join(','); | |
| log(`Автоопределено ${lastNames.length} фамилий`); | |
| } | |
| } | |
| if (el('autoDetectSuffixes').checked) { | |
| const suffixes = Array.from(state.detectedSuffixes).slice(0, 15); | |
| if (suffixes.length > 0 && !el('manualSuffixes').value) { | |
| el('manualSuffixes').value = suffixes.join(','); | |
| log(`Автоопределено ${suffixes.length} суффиксов`); | |
| } | |
| } | |
| renderPatternTable(); | |
| renderDomainTable(); | |
| state.analyzed = true; | |
| log('Анализ завершён.'); | |
| } | |
| async function streamFileProcessing(file){ | |
| const decoder = new TextDecoder(); | |
| const reader = file.stream().getReader(); | |
| let { value: chunk, done: readerDone } = await reader.read(); | |
| let buffer = ''; | |
| while (!readerDone) { | |
| buffer += decoder.decode(chunk, { stream: true }); | |
| const lines = buffer.split(/\r\n|\n/); | |
| buffer = lines.pop(); | |
| await processLineBatch(lines); | |
| const res = await reader.read(); | |
| chunk = res.value; readerDone = res.done; | |
| } | |
| if (buffer) await processLineBatch([buffer]); | |
| await reader.releaseLock?.(); | |
| } | |
| async function fileReaderProcessing(file){ | |
| const CHUNK = 2 * 1024 * 1024; | |
| let offset = 0; | |
| while (offset < file.size) { | |
| const slice = file.slice(offset, offset + CHUNK); | |
| const text = await slice.text(); | |
| const lines = text.split(/\r\n|\n/); | |
| if (offset + CHUNK < file.size) { | |
| const last = lines.pop(); | |
| await processLineBatch(lines); | |
| offset += CHUNK - (last ? last.length : 0); | |
| } else { | |
| await processLineBatch(lines); | |
| offset += CHUNK; | |
| } | |
| await new Promise(r => setTimeout(r, 0)); | |
| } | |
| } | |
| async function processLineBatch(lines) { | |
| for (const rawLine of lines) { | |
| const line = (rawLine||'').trim(); | |
| if (!line) continue; | |
| state.totalLines++; | |
| const atIdx = line.indexOf('@'); | |
| const local = safeLower(atIdx === -1 ? line : line.slice(0, atIdx)); | |
| const domain = atIdx === -1 ? '' : safeLower(line.slice(atIdx+1)); | |
| if (!local) continue; | |
| state.uniqueLocalparts.add(local); | |
| if (domain) state.domainCounts[domain] = (state.domainCounts[domain]||0) + 1; | |
| classifyLocal(local); | |
| extractNamesFromLocal(local); | |
| extractSuffixesFromLocal(local); | |
| if (state.sampleLocalparts.length < 200) state.sampleLocalparts.push({local, domain}); | |
| } | |
| if (state.totalLines % 500 === 0) { | |
| el('statLines').textContent = String(state.totalLines); | |
| el('statUnique').textContent = String(state.uniqueLocalparts.size); | |
| el('statDomains').textContent = String(Object.keys(state.domainCounts).length); | |
| await new Promise(r=>setTimeout(r,0)); | |
| } | |
| } | |
| function classifyLocal(local){ | |
| if (PATTERNS['fn.dot.ln'].test(local)) { | |
| state.patternCounts['fn.dot.ln']++; | |
| addExample('fn.dot.ln', local); | |
| return; | |
| } | |
| if (PATTERNS['fi.dot.ln'].test(local)) { | |
| state.patternCounts['fi.dot.ln']++; | |
| addExample('fi.dot.ln', local); | |
| return; | |
| } | |
| if (PATTERNS['nick_digits'].test(local)) { | |
| state.patternCounts['nick_digits']++; | |
| addExample('nick_digits', local); | |
| return; | |
| } | |
| if (PATTERNS['initials'].test(local)) { | |
| state.patternCounts['initials']++; | |
| addExample('initials', local); | |
| return; | |
| } | |
| if (PATTERNS['nick'].test(local)) { | |
| state.patternCounts['nick']++; | |
| addExample('nick', local); | |
| return; | |
| } | |
| if (PATTERNS['contains_special'].test(local)) { | |
| state.patternCounts['contains_special']++; | |
| addExample('contains_special', local); | |
| return; | |
| } | |
| state.patternCounts['fnln']++; | |
| addExample('fnln', local); | |
| } | |
| function addExample(pat, local) { | |
| const s = state.patternExamples[pat]; | |
| if (s && s.size < 8) s.add(local); | |
| } | |
| function extractNamesFromLocal(local) { | |
| const fnLnMatch = local.match(/^([a-zäöüß]{2,20})[._]([a-zäöüß]{2,20})/i); | |
| if (fnLnMatch) { | |
| state.detectedFirstNames.add(fnLnMatch[1]); | |
| state.detectedLastNames.add(fnLnMatch[2]); | |
| } | |
| const fnDigitsMatch = local.match(/^([a-zäöüß]{2,20})[0-9]{2,6}$/i); | |
| if (fnDigitsMatch) { | |
| state.detectedFirstNames.add(fnDigitsMatch[1]); | |
| } | |
| const lnDigitsMatch = local.match(/^[a-zäöüß]{1}[._]([a-zäöüß]{2,20})[0-9]{2,6}$/i); | |
| if (lnDigitsMatch) { | |
| state.detectedLastNames.add(lnDigitsMatch[1]); | |
| } | |
| const initialsLnMatch = local.match(/^[a-z]{1,2}[._]([a-zäöüß]{2,20})/i); | |
| if (initialsLnMatch) { | |
| state.detectedLastNames.add(initialsLnMatch[1]); | |
| } | |
| } | |
| function extractSuffixesFromLocal(local) { | |
| const digitsMatch = local.match(/[a-zäöüß][._-]?([0-9]{2,6})$/i); | |
| if (digitsMatch) { | |
| state.detectedSuffixes.add(digitsMatch[1]); | |
| } | |
| const yearMatch = local.match(/(19[0-9]{2}|20[0-9]{2})$/); | |
| if (yearMatch) { | |
| state.detectedSuffixes.add(yearMatch[1]); | |
| } | |
| const shortNumMatch = local.match(/([0-9]{2,4})$/); | |
| if (shortNumMatch && !yearMatch) { | |
| state.detectedSuffixes.add(shortNumMatch[1]); | |
| } | |
| } | |
| function renderPatternTable(){ | |
| const tbody = el('patternsTable').querySelector('tbody'); | |
| tbody.innerHTML = ''; | |
| const total = sum(state.patternCounts) || 1; | |
| Object.entries(state.patternCounts).sort((a,b)=>b[1]-a[1]).forEach(([p,c])=>{ | |
| const tr = document.createElement('tr'); | |
| const examples = [...(state.patternExamples[p]||[])].slice(0,5).join(', '); | |
| const pct = ((c/total)*100).toFixed(1) + '%'; | |
| tr.innerHTML = `<td>${p}</td><td>${c} (${pct})</td><td class="small">${examples}</td>`; | |
| tbody.appendChild(tr); | |
| }); | |
| } | |
| function renderDomainTable(){ | |
| const tbody = el('domainsTable').querySelector('tbody'); | |
| tbody.innerHTML = ''; | |
| Object.entries(state.domainCounts).sort((a,b)=>b[1]-a[1]).slice(0,100).forEach(([d,c])=>{ | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = `<td>@${d}</td><td>${c}</td>`; | |
| tbody.appendChild(tr); | |
| }); | |
| } | |
| function buildGenerators() { | |
| const patternWeights = {}; | |
| Object.keys(state.patternCounts).forEach(k => { | |
| patternWeights[k] = state.patternCounts[k] || 1; | |
| }); | |
| let domainWeights = {}; | |
| if (el('useManualDomain').checked && el('manualDomain').value.trim()) { | |
| const manualDomain = safeLower(el('manualDomain').value); | |
| domainWeights = {}; | |
| domainWeights[manualDomain] = 1000; | |
| log(`Используется ручной домен: ${manualDomain}`); | |
| } else { | |
| Object.entries(state.domainCounts).forEach(([d,w]) => domainWeights[d] = w); | |
| if (Object.keys(domainWeights).length === 0) { | |
| ['gmail.com','gmx.at','aon.at','yahoo.com','outlook.com','web.de','chello.at','liwest.at','hotmail.com'].forEach(d=>domainWeights[d] = 10); | |
| } | |
| if (el('preferLocalDomains').checked) { | |
| const localPrefer = ['gmx.at','aon.at','chello.at','liwest.at','inode.at','student.uibk.ac.at','proton.me','protonmail.com']; | |
| localPrefer.forEach(d=>{ | |
| if (domainWeights[d]) domainWeights[d] *= 3; | |
| }); | |
| } | |
| } | |
| const manual = el('manualSuffixes').value.split(',').map(s=>s.trim()).filter(Boolean); | |
| const manualSuffixes = uniqArray(manual); | |
| const providedFirsts = el('sampleNames').value.split(',').map(x=>safeLower(x)).filter(Boolean); | |
| const providedLasts = el('sampleLastnames').value.split(',').map(x=>safeLower(x)).filter(Boolean); | |
| const guessedFirsts = new Set(); | |
| const guessedLasts = new Set(); | |
| state.sampleLocalparts.forEach(({local})=>{ | |
| const m = local.match(/^([a-zäöüß]{2,20})[._]([a-zäöüß]{2,20})/i); | |
| if (m) { guessedFirsts.add(m[1]); guessedLasts.add(m[2]); } | |
| else { | |
| const m2 = local.match(/^([a-zäöüß]{2,20})[0-9]{2,6}$/i); | |
| if (m2) guessedFirsts.add(m2[1]); | |
| } | |
| }); | |
| const firstPool = uniqArray([...providedFirsts, ...Array.from(guessedFirsts)]).filter(Boolean); | |
| const lastPool = uniqArray([...providedLasts, ...Array.from(guessedLasts)]).filter(Boolean); | |
| if (firstPool.length === 0) firstPool.push('alex','maria','stefan','anna','thomas','martin'); | |
| if (lastPool.length === 0) lastPool.push('bauer','schmidt','mueller','hofer','gruber','wagner'); | |
| const specialLocals = Array.from(state.uniqueLocalparts).filter(x => PATTERNS['contains_special'].test(x)); | |
| const gens = { | |
| 'fn.dot.ln': () => { | |
| const fn = firstPool[Math.floor(Math.random()*firstPool.length)]; | |
| const ln = lastPool[Math.floor(Math.random()*lastPool.length)]; | |
| return `${fn}.${ln}`; | |
| }, | |
| 'fi.dot.ln': () => { | |
| const fn = firstPool[Math.floor(Math.random()*firstPool.length)]; | |
| const ln = lastPool[Math.floor(Math.random()*lastPool.length)]; | |
| return `${fn[0]}.${ln}`; | |
| }, | |
| 'fnln': () => { | |
| const fn = firstPool[Math.floor(Math.random()*firstPool.length)]; | |
| const ln = lastPool[Math.floor(Math.random()*lastPool.length)]; | |
| return `${fn}${ln}`; | |
| }, | |
| 'nick_digits': () => { | |
| const pick = Math.random()<0.5 ? firstPool[Math.floor(Math.random()*firstPool.length)] : (sampleNickFromDataset() || firstPool[Math.floor(Math.random()*firstPool.length)]); | |
| const digits = getRandomSuffix(); | |
| return `${pick}${digits}`; | |
| }, | |
| 'nick': () => { | |
| return sampleNickFromDataset() || firstPool[Math.floor(Math.random()*firstPool.length)]; | |
| }, | |
| 'initials': () => { | |
| const fn = firstPool[Math.floor(Math.random()*firstPool.length)]; | |
| const ln = lastPool[Math.floor(Math.random()*lastPool.length)]; | |
| return `${fn[0]}${ln[0]}`; | |
| }, | |
| 'contains_special': () => { | |
| if (specialLocals.length > 0) { | |
| return specialLocals[Math.floor(Math.random()*specialLocals.length)]; | |
| } | |
| return sampleNickFromDataset() || 'user' + Math.floor(100+Math.random()*899); | |
| } | |
| }; | |
| function sampleNickFromDataset(){ | |
| const arr = Array.from(state.uniqueLocalparts).filter(x => /^[a-z][a-z0-9._-]{1,20}$/i.test(x)); | |
| if (arr.length === 0) return null; | |
| return arr[Math.floor(Math.random()*arr.length)].replace(/[._-]+$/,'').replace(/\s+/g,''); | |
| } | |
| function getRandomSuffix(){ | |
| const ms = manualSuffixes.filter(Boolean); | |
| const picks = []; | |
| if (ms.length) picks.push(...ms); | |
| const common = ['007','123','69','84','97','2000','2001','2010','2012']; | |
| picks.push(...common); | |
| picks.push(String(100 + Math.floor(Math.random()*899)).slice(1)); | |
| return picks[Math.floor(Math.random()*picks.length)]; | |
| } | |
| return {patternWeights, domainWeights, gens, firstPool, lastPool}; | |
| } | |
| async function generateToBlob(count, opts) { | |
| const {patternWeights, domainWeights, gens} = buildGenerators(); | |
| const encoder = new TextEncoder(); | |
| const chunks = []; | |
| const dedupeSet = opts.dedupe ? new Set() : null; | |
| let produced = 0; | |
| const totalTarget = count; | |
| const batchSize = 10000; | |
| let buffer = []; | |
| let bufferBytes = 0; | |
| function pickPattern() { | |
| return pickWeighted(patternWeights); | |
| } | |
| function pickDomain(){ | |
| return pickWeighted(domainWeights); | |
| } | |
| function genOne(){ | |
| const pat = pickPattern(); | |
| const local = (gens[pat] ? gens[pat]() : gens['fnln']()).replace(/\s+/g,'').toLowerCase(); | |
| const domain = pickDomain(); | |
| return {local, domain, pattern: pat}; | |
| } | |
| const fmt = opts.exportFormat || 'txt'; | |
| if (fmt === 'json') { | |
| chunks.push(encoder.encode('[')); | |
| } | |
| while (produced < totalTarget) { | |
| const toMake = Math.min(batchSize, totalTarget - produced); | |
| for (let i=0; i<toMake; i++) { | |
| const item = genOne(); | |
| const lineTxt = `${item.local}@${item.domain}`; | |
| if (dedupeSet) { | |
| if (dedupeSet.has(lineTxt)) continue; | |
| dedupeSet.add(lineTxt); | |
| } | |
| let line; | |
| if (fmt === 'txt') { | |
| line = lineTxt + '\n'; | |
| } else if (fmt === 'csv') { | |
| line = `"${item.local}","${item.domain}","${item.pattern}"\n`; | |
| } else if (fmt === 'json') { | |
| line = (produced > 0 ? ',' : '') + JSON.stringify({local:item.local,domain:item.domain,pattern:item.pattern}) + '\n'; | |
| } | |
| buffer.push(line); | |
| bufferBytes += line.length; | |
| produced++; | |
| if (buffer.length >= 5000 || bufferBytes > 1024 * 1024) { | |
| flushBuffer(); | |
| } | |
| } | |
| if (opts.progressCallback && produced % 5000 === 0) { | |
| const currentCount = dedupeSet ? dedupeSet.size : produced; | |
| opts.progressCallback(Math.min(100, Math.round((currentCount / totalTarget) * 100)), currentCount); | |
| await new Promise(r => requestAnimationFrame(r)); | |
| } | |
| } | |
| if (buffer.length > 0) { | |
| flushBuffer(); | |
| } | |
| if (fmt === 'json') { | |
| chunks.push(encoder.encode(']')); | |
| } | |
| return new Blob(chunks, {type: fmt === 'json' ? 'application/json;charset=utf-8' : 'text/plain;charset=utf-8'}); | |
| function flushBuffer() { | |
| if (buffer.length === 0) return; | |
| const text = buffer.join(''); | |
| chunks.push(encoder.encode(text)); | |
| buffer = []; | |
| bufferBytes = 0; | |
| } | |
| } | |
| let lastGeneratedBlobURL = null; | |
| async function handleGenerate() { | |
| if (!state.analyzed) { | |
| alert('Сначала выполните анализ файла (Analyze file).'); | |
| return; | |
| } | |
| const count = Math.max(1, parseInt(el('genCount').value || '100',10)); | |
| const format = el('exportFormat').value; | |
| const dedupe = el('dedupe').checked; | |
| el('generateBtn').disabled = true; | |
| el('generateBtn').textContent = 'Генерация...'; | |
| log(`Старт генерации: ${count} элементов, формат=${format}, dedupe=${dedupe}`); | |
| try { | |
| const blob = await generateToBlob(count, { | |
| dedupe, | |
| exportFormat: format, | |
| progressCallback: (pct, current) => { | |
| el('progressBar').style.width = pct + '%'; | |
| el('progressBar').setAttribute('aria-valuenow', pct); | |
| el('previewBox').textContent = `Генерация: ${current} / ${count} (${pct}%)`; | |
| } | |
| }); | |
| if (lastGeneratedBlobURL) URL.revokeObjectURL(lastGeneratedBlobURL); | |
| lastGeneratedBlobURL = URL.createObjectURL(blob); | |
| el('downloadBtn').disabled = false; | |
| el('downloadBtn').textContent = `Скачать (${(blob.size/1024/1024).toFixed(2)} MB)`; | |
| el('downloadBtn').onclick = () => { | |
| const a = document.createElement('a'); | |
| let ext = '.txt'; | |
| if (format==='csv') ext = '.csv'; | |
| if (format==='json') ext = '.json'; | |
| a.href = lastGeneratedBlobURL; | |
| a.download = `generated_usernames${ext}`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| a.remove(); | |
| }; | |
| const previewLimit = Math.max(10, Math.min(1000, parseInt(el('previewLimit').value || '50',10))); | |
| const previewTxt = await readBlobHead(blob, previewLimit); | |
| el('previewBox').textContent = previewTxt; | |
| // Apply filter after generation | |
| const filter = (el('previewFilter').value || '').toLowerCase(); | |
| if (filter) { | |
| const lines = previewTxt.split(/\r\n|\n/).filter(Boolean); | |
| const shown = lines.filter(l => l.includes(filter)).slice(0, previewLimit); | |
| el('previewBox').textContent = shown.join('\n'); | |
| } | |
| log('Генерация завершена успешно.'); | |
| } catch (e) { | |
| log('Ошибка при генерации: ' + (e && e.message ? e.message : String(e))); | |
| alert('Ошибка генерации: ' + (e && e.message ? e.message : String(e))); | |
| } finally { | |
| el('generateBtn').disabled = false; | |
| el('generateBtn').textContent = 'Сгенерировать'; | |
| el('progressBar').style.width = '0%'; | |
| el('progressBar').setAttribute('aria-valuenow', 0); | |
| } | |
| } | |
| async function readBlobHead(blob, limitLines){ | |
| const CHUNK = 64 * 1024; | |
| let offset = 0; | |
| let decoder = new TextDecoder(); | |
| let accumulated = ''; | |
| while (offset < blob.size && accumulated.split(/\r\n|\n/).length - 1 < limitLines) { | |
| const slice = blob.slice(offset, offset + CHUNK); | |
| const text = await slice.text(); | |
| accumulated += text; | |
| offset += CHUNK; | |
| await new Promise(r=>setTimeout(r,0)); | |
| } | |
| const lines = accumulated.split(/\r\n|\n/).filter(Boolean).slice(0, limitLines); | |
| return lines.join('\n'); | |
| } | |
| el('analyzeBtn').addEventListener('click', async () => { | |
| const file = el('fileInput').files && el('fileInput').files[0]; | |
| if (!file) { alert('Выберите файл .txt со списком логинов.'); return; } | |
| try { | |
| el('analyzeBtn').disabled = true; | |
| el('analyzeBtn').textContent = 'Анализ...'; | |
| await processFile(file); | |
| } catch (e) { | |
| log('Ошибка при анализе: ' + (e && e.message ? e.message : String(e))); | |
| alert('Ошибка при анализе файла: ' + (e && e.message ? e.message : String(e))); | |
| } finally { | |
| el('analyzeBtn').disabled = false; | |
| el('analyzeBtn').textContent = 'Проанализировать файл'; | |
| } | |
| }); | |
| el('resetBtn').addEventListener('click', () => { | |
| if (!confirm('Сбросить текущее состояние анализа?')) return; | |
| state = { | |
| totalLines: 0, | |
| uniqueLocalparts: new Set(), | |
| domainCounts: Object.create(null), | |
| patternCounts: Object.create(null), | |
| patternExamples: Object.create(null), | |
| sampleLocalparts: [], | |
| domainsList: [], | |
| manualSuffixes: [], | |
| detectedFirstNames: new Set(), | |
| detectedLastNames: new Set(), | |
| detectedSuffixes: new Set(), | |
| analyzed: false | |
| }; | |
| Object.keys(PATTERNS).forEach(k => { state.patternCounts[k]=0; state.patternExamples[k]=new Set(); }); | |
| el('statLines').textContent = '0'; | |
| el('statUnique').textContent = '0'; | |
| el('statDomains').textContent = '0'; | |
| el('patternsTable').querySelector('tbody').innerHTML = ''; | |
| el('domainsTable').querySelector('tbody').innerHTML = ''; | |
| el('previewBox').textContent = ''; | |
| el('logBox').textContent = ''; | |
| el('downloadBtn').disabled = true; | |
| if (lastGeneratedBlobURL) { URL.revokeObjectURL(lastGeneratedBlobURL); lastGeneratedBlobURL = null; } | |
| log('Состояние сброшено пользователем.'); | |
| }); | |
| el('generateBtn').addEventListener('click', handleGenerate); | |
| el('previewBtn').addEventListener('click', () => { | |
| if (!state.analyzed) { alert('Сначала выполните анализ файла.'); return; } | |
| const limit = Math.max(10, Math.min(200, parseInt(el('previewLimit').value || '50',10))); | |
| const filter = (el('previewFilter').value || '').toLowerCase(); | |
| const samples = state.sampleLocalparts.map(x => `${x.local}@${x.domain||'[no-domain]'}`); | |
| const shown = (filter ? samples.filter(s => s.includes(filter)) : samples).slice(0, limit); | |
| el('previewBox').textContent = shown.join('\n'); | |
| }); | |
| el('copyPreview').addEventListener('click', async () => { | |
| const txt = el('previewBox').textContent.trim(); | |
| if (!txt) return alert('Нет текста для копирования.'); | |
| try { | |
| await navigator.clipboard.writeText(txt); | |
| log('Предпросмотр скопирован в буфер обмена.'); | |
| } catch (e) { | |
| alert('Не удалось скопировать: ' + (e.message||e)); | |
| } | |
| }); | |
| el('previewFilter').addEventListener('input', () => { | |
| const filter = (el('previewFilter').value || '').toLowerCase(); | |
| if (!el('previewBox').textContent) return; | |
| const lines = el('previewBox').textContent.split(/\r\n|\n/).filter(Boolean); | |
| const shown = filter ? lines.filter(l => l.includes(filter)) : lines; | |
| el('previewBox').textContent = shown.slice(0, Math.max(50, parseInt(el('previewLimit').value||'50'))).join('\n'); | |
| }); | |
| el('fileInput').addEventListener('change', () => { | |
| el('statLines').textContent = '0'; | |
| el('statUnique').textContent = '0'; | |
| el('statDomains').textContent = '0'; | |
| el('patternsTable').querySelector('tbody').innerHTML = ''; | |
| el('domainsTable').querySelector('tbody').innerHTML = ''; | |
| el('previewBox').textContent = ''; | |
| el('logBox').textContent = ''; | |
| state.analyzed = false; | |
| if (lastGeneratedBlobURL) { URL.revokeObjectURL(lastGeneratedBlobURL); lastGeneratedBlobURL = null; el('downloadBtn').disabled = true; } | |
| log('Новый файл выбран, предыдущий анализ отменён.'); | |
| }); | |
| log('Готов к работе. Загрузите файл и нажмите "Проанализировать файл".'); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |