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/target</title> | |
| <style> | |
| :root { | |
| --bg: #0b0f14; | |
| --card: #0f1720; | |
| --muted: #9aa6b2; | |
| --text: #e6eef6; | |
| --accent: #4fc3f7; | |
| --good: #7be495; | |
| --danger: #ff6b6b; | |
| --warning: #ffd166; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| html, body { | |
| height: 100%; | |
| margin: 0; | |
| font-family: 'Segoe UI', Roboto, Arial, sans-serif; | |
| background: linear-gradient(180deg, #061018, #07131a); | |
| color: var(--text); | |
| } | |
| .app { | |
| max-width: 1200px; | |
| margin: 24px auto; | |
| padding: 18px; | |
| border-radius: 12px; | |
| background: linear-gradient(180deg, #08121a, #07101a); | |
| box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6); | |
| } | |
| h1 { | |
| margin: 0 0 8px; | |
| font-size: 20px; | |
| } | |
| .grid { | |
| display: grid; | |
| grid-template-columns: 360px 1fr; | |
| gap: 16px; | |
| } | |
| .card { | |
| background: var(--card); | |
| padding: 14px; | |
| border-radius: 10px; | |
| border: 1px solid rgba(255, 255, 255, 0.02); | |
| } | |
| label { | |
| display: block; | |
| font-size: 13px; | |
| color: var(--muted); | |
| margin-top: 10px; | |
| } | |
| 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: #07111a; | |
| color: var(--text); | |
| outline: none; | |
| font-size: 13px; | |
| } | |
| textarea { | |
| min-height: 90px; | |
| resize: vertical; | |
| font-family: ui-monospace, Menlo, monospace; | |
| } | |
| .row { | |
| display: flex; | |
| gap: 8px; | |
| align-items: center; | |
| } | |
| .btn { | |
| padding: 10px 12px; | |
| border-radius: 8px; | |
| border: 0; | |
| background: linear-gradient(90deg, var(--accent), #7be495); | |
| color: #042028; | |
| font-weight: 700; | |
| cursor: pointer; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .btn.alt { | |
| background: transparent; | |
| border: 1px solid rgba(255, 255, 255, 0.04); | |
| color: var(--text); | |
| } | |
| .btn.danger { | |
| background: linear-gradient(90deg, var(--danger), #ff8e8e); | |
| color: #2d0000; | |
| } | |
| .btn:hover { | |
| opacity: 0.9; | |
| } | |
| .btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .small { | |
| font-size: 12px; | |
| color: var(--muted); | |
| } | |
| .stats { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 8px; | |
| margin-top: 8px; | |
| } | |
| .stat { | |
| background: linear-gradient(180deg, rgba(255, 255, 255, 0.01), transparent); | |
| padding: 8px; | |
| border-radius: 8px; | |
| border: 1px solid rgba(255, 255, 255, 0.02); | |
| } | |
| .list { | |
| max-height: 60vh; | |
| overflow: auto; | |
| padding: 8px; | |
| border-radius: 8px; | |
| border: 1px solid rgba(255, 255, 255, 0.02); | |
| background: #07121a; | |
| } | |
| .pattern-item { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 6px 8px; | |
| border-radius: 6px; | |
| margin-bottom: 6px; | |
| background: rgba(255, 255, 255, 0.01); | |
| } | |
| .pattern-item .bar { | |
| height: 8px; | |
| background: linear-gradient(90deg, var(--accent), #7be495); | |
| border-radius: 6px; | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| margin-top: 10px; | |
| } | |
| .switch { | |
| display: flex; | |
| gap: 8px; | |
| align-items: center; | |
| } | |
| .chip { | |
| padding: 6px 8px; | |
| border-radius: 999px; | |
| background: rgba(255, 255, 255, 0.02); | |
| font-size: 13px; | |
| } | |
| .footer { | |
| margin-top: 12px; | |
| font-size: 12px; | |
| color: var(--muted); | |
| } | |
| input[type="file"] { | |
| color: var(--text); | |
| } | |
| .progress { | |
| height: 10px; | |
| background: rgba(255, 255, 255, 0.03); | |
| border-radius: 6px; | |
| overflow: hidden; | |
| margin-top: 8px; | |
| } | |
| .progress > i { | |
| display: block; | |
| height: 100%; | |
| width: 0; | |
| background: linear-gradient(90deg, var(--accent), #7be495); | |
| } | |
| .table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin-top: 8px; | |
| } | |
| .table th, .table td { | |
| padding: 6px; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.02); | |
| text-align: left; | |
| font-size: 13px; | |
| } | |
| .controls-right { | |
| display: flex; | |
| gap: 8px; | |
| align-items: center; | |
| } | |
| .export-btn { | |
| background: #20303b; | |
| border-radius: 8px; | |
| color: var(--text); | |
| padding: 8px 10px; | |
| border: 1px solid rgba(255, 255, 255, 0.03); | |
| cursor: pointer; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .export-btn:hover { | |
| background: #2a3f4f; | |
| } | |
| .muted { | |
| color: var(--muted); | |
| } | |
| .section-title { | |
| font-size: 14px; | |
| font-weight: 600; | |
| margin: 16px 0 8px 0; | |
| padding-bottom: 4px; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .custom-pattern { | |
| background: rgba(255, 255, 255, 0.02); | |
| padding: 10px; | |
| border-radius: 6px; | |
| margin-bottom: 8px; | |
| } | |
| .pattern-controls { | |
| display: flex; | |
| gap: 8px; | |
| margin-top: 8px; | |
| } | |
| .pattern-controls .btn { | |
| padding: 6px 10px; | |
| font-size: 12px; | |
| } | |
| .pattern-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| } | |
| .pattern-name { | |
| font-weight: 500; | |
| font-size: 13px; | |
| } | |
| .pattern-template { | |
| font-family: ui-monospace, Menlo, monospace; | |
| font-size: 12px; | |
| color: var(--accent); | |
| } | |
| .pattern-enabled { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .preset-section { | |
| background: rgba(255, 255, 255, 0.03); | |
| padding: 12px; | |
| border-radius: 8px; | |
| margin: 12px 0; | |
| } | |
| .preset-title { | |
| font-weight: 600; | |
| margin-bottom: 8px; | |
| color: var(--accent); | |
| } | |
| @media (max-width: 980px) { | |
| .grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app" role="application" aria-label="Advanced username generator"> | |
| <header style="display:flex;justify-content:space-between;align-items:center;gap:12px"> | |
| <div> | |
| <h1>Advanced leak-based username generator</h1> | |
| <div class="small">Загрузите очищенный список логинов (email) — анализируем паттерны, домены и генерируем реалистичные вариации.</div> | |
| </div> | |
| <div class="chip">Target: willhaben / custom</div> | |
| </header> | |
| <div class="grid" style="margin-top:12px"> | |
| <!-- LEFT: Controls + analysis --> | |
| <div class="card" aria-live="polite"> | |
| <label>1) Загрузить файл с логинами (.txt)</label> | |
| <input id="fileInput" type="file" accept=".txt" /> | |
| <div class="small">Поддерживает большие файлы — обработка в воркере и по чанкам. Один email на строку.</div> | |
| <div class="progress" title="progress"><i id="fileProgress"></i></div> | |
| <label>1.1) Загрузить done.txt (уже сгенерированные логины для пропуска)</label> | |
| <input id="doneFileInput" type="file" accept=".txt" /> | |
| <div class="small">Загрузите файл с уже сгенерированными логинами для исключения дубликатов</div> | |
| <div class="small muted" id="doneCount">Загружено: 0 логинов</div> | |
| <label>1.2) Импорт настроек (.json)</label> | |
| <input id="settingsFileInput" type="file" accept=".json" /> | |
| <div class="small">Загрузите сохраненные настройки для восстановления конфигурации</div> | |
| <label style="margin-top:10px">2) Настройки анализа</label> | |
| <div class="small">Нормализация: диакритика → латинские аналоги, lowercase, удаление пробелов</div> | |
| <div style="display:flex;gap:8px;margin-top:8px"> | |
| <label class="switch"><input id="optNormalize" type="checkbox" checked /> <span class="small">Нормализация</span></label> | |
| <label class="switch"><input id="optExtractNames" type="checkbox" checked /> <span class="small">Экстракт имен/фамилий</span></label> | |
| </div> | |
| <div class="section-title">3) Предустановки</div> | |
| <div class="preset-section"> | |
| <div class="preset-title">Немецкие фамилии</div> | |
| <textarea id="lastSeeds" style="height:120px">müller,schmidt,schneider,fischer,weber,meyer,wagner,becker,schulz,hoffmann,schäfer,koch,bauer,richter,klein,wolf,schröder,neumann,schwarz,braun,hofmann,zimmermann,schmitt,hartmann,krüger,schmid,weiss,scholz,maier,köhler,herrmann,lange,schulte,krause,meier,lehmann,schubert,kühn,vogel,peters,fritz</textarea> | |
| </div> | |
| <div class="preset-section"> | |
| <div class="preset-title">Немецкие имена</div> | |
| <textarea id="firstSeeds" style="height:120px">max,anna,thomas,maria,michael,sabine,andreas,julia,stefan,sandra,peter,christine,klaus,angelika,wolfgang,monika,jürgen,petra,frank,birgit,hans,uta,ralf,susanne,karl,elke,uwe,kirsten,bernd,heike,lukas,sarah,martin,katrin,christoph,nicole,dirk,johanna,rainer,diana,marcus,sylvia,matthias,nina,jan,simone,alexander,claudia,daniel,corinna,stefanie,andrea,patrick,tanja,christian,jessica,oliver,melanie,marcel,anja,tobias,jana,manuel,sabrina,philipp,carina,marco,lena,christina,alexandra,florian,miriam,bastian,nadia,dennis,verena,serdar,jennifer,tim,sonja,rene,antje,mario,silke,dominik,bianca,eric,nadine,fabian,yvonne,kai,ramona,steffen,ines,marius,elena,kristian,patricia,robert,svenja,sebastian,jennifer,nicolas,anika,jens,irene,timo,maren,jörg,eva,volker,anke,heiko,annette,maik,susann,torsten,dagmar,ingo,katja,udo,regina,harald,ilona,lothar,gabriele,gerhard,ute,dieter,brigitte,walter,helga,bernhard,ursula,hermann,elisabeth,kurt,margrit,alfred,gisela,heinz,renate,ernst,hildegard,werner,ingrid,günther,christel,karl-heinz,marianne,franz,barbara,hugo,elke,fritz,anneliese</textarea> | |
| </div> | |
| <div class="section-title">4) Кастомные шаблоны</div> | |
| <div id="customPatternsContainer"></div> | |
| <button id="addPatternBtn" class="btn alt" style="margin-top:8px"> | |
| <span>+</span> Добавить шаблон | |
| </button> | |
| <div class="small muted">Переменные: {first} {last} {nick} {digit} {year}</div> | |
| <div class="controls" style="margin-top:10px"> | |
| <button id="analyzeBtn" class="btn alt">Анализировать файл</button> | |
| <button id="resetBtn" class="btn alt">Сбросить</button> | |
| <button id="applyPresetsBtn" class="btn">Применить предустановки</button> | |
| </div> | |
| <div class="stats" style="margin-top:12px"> | |
| <div class="stat"><div class="small">Всего записей</div><div id="statTotal">0</div></div> | |
| <div class="stat"><div class="small">Уникальных локал-партий</div><div id="statUnique">0</div></div> | |
| <div class="stat"><div class="small">Популярный домен</div><div id="statTopDomain">—</div></div> | |
| <div class="stat"><div class="small">Шаблонов распознано</div><div id="statPatterns">—</div></div> | |
| </div> | |
| <label style="margin-top:12px">Результат анализа — паттерны (клик для выбора/исключения)</label> | |
| <div id="patternList" class="list" style="min-height:120px"></div> | |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-top:10px"> | |
| <div class="small muted">Авто-расстановка весов паттернов по частоте (можно отредактировать)</div> | |
| <div class="small muted">Доля доменов и суффиксов вычислена из файла</div> | |
| </div> | |
| </div> | |
| <!-- RIGHT: generation & export --> | |
| <div class="card"> | |
| <div style="display:flex;justify-content:space-between;align-items:center"> | |
| <div> | |
| <label>5) Параметры генерации</label> | |
| <div class="small">Выберите паттерны и домены, затем задайте объём генерации и нажмите "Генерировать".</div> | |
| </div> | |
| <div class="controls-right"> | |
| <button id="openSampleBtn" class="export-btn"> | |
| <span>ℹ</span> Показать пример | |
| </button> | |
| <button id="genBtn" class="btn"> | |
| <span>▶</span> Генерировать | |
| </button> | |
| </div> | |
| </div> | |
| <div style="display:flex;gap:8px;margin-top:10px"> | |
| <div style="flex:1"> | |
| <label>Кол-во генерируемых</label> | |
| <input id="genCount" type="number" value="200" min="1" /> | |
| </div> | |
| <div style="width:160px"> | |
| <label>Домен приоритет</label> | |
| <select id="domainPreset"> | |
| <option value="auto">Авто (из файла)</option> | |
| <option value="local">Локальные (gmx.at, aon.at...)</option> | |
| <option value="global">Global (gmail,yahoo,outlook)</option> | |
| <option value="custom" selected>Жестко заданный домен</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div id="customDomainContainer" style="margin-top:10px"> | |
| <label>Жестко заданный домен</label> | |
| <input id="customDomain" type="text" value="gmx.de" placeholder="example.com" /> | |
| <div class="small muted">Все сгенерированные логины будут использовать этот домен</div> | |
| </div> | |
| <label style="margin-top:10px">Настройки суффиксов / чисел</label> | |
| <div style="display:flex;gap:8px"> | |
| <input id="sufCommon" placeholder="Популярные суффиксы, через запятую (007,123,84,2005)" /> | |
| <input id="sufYears" placeholder="Годы (префикс/диапазон) например 1960-2005" /> | |
| </div> | |
| <label style="margin-top:10px">Входные источники (используются при генерации)</label> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px"> | |
| <textarea id="firstPool" placeholder="first names pool (one,comma,or newline separated)"></textarea> | |
| <textarea id="lastPool" placeholder="last names pool"></textarea> | |
| </div> | |
| <div class="controls" style="margin-top:10px"> | |
| <button id="previewBtn" class="btn alt"> | |
| <span>👁</span> Предпросмотр 25 | |
| </button> | |
| <button id="exportTxt" class="export-btn"> | |
| <span>💾</span> Экспорт .txt | |
| </button> | |
| <button id="exportCsv" class="export-btn"> | |
| <span>💾</span> Экспорт .csv | |
| </button> | |
| <button id="exportJson" class="export-btn"> | |
| <span>💾</span> Экспорт .json | |
| </button> | |
| <button id="exportSettings" class="export-btn"> | |
| <span>⚙</span> Экспорт настроек | |
| </button> | |
| <button id="exportDone" class="export-btn"> | |
| <span>✓</span> Экспорт done.txt | |
| </button> | |
| </div> | |
| <label style="margin-top:12px">Сгенерированные логины (дубликаты удалены автоматически)</label> | |
| <div id="outList" class="list" style="min-height:200px;white-space:pre-wrap;font-family:ui-monospace,Menlo,monospace"></div> | |
| <div class="footer">Гарантии: инструмент локально обрабатывает файлы в браузере. Для очень больших файлов (>100MB) рекомендуется использовать современные браузеры и больше памяти. Воркер/чанковая обработка активированы для производительности.</div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| /* | |
| Robust client-side implementation: | |
| - Uses Web Worker (inline via Blob) for chunked file parsing + pattern extraction. | |
| - Defensive programming: try/catch, time-slicing, progress updates. | |
| - Pattern extraction: multiple regex classes + domain tally + suffix detection. | |
| - Weighted generation by observed frequencies, with user overrides. | |
| - Exports: txt, csv, json. Dedup & normalization. | |
| - All code contained in single file; no external dependencies. | |
| */ | |
| /* ---------- Utils & Normalization ---------- */ | |
| const DIACRIT_MAP = { | |
| 'ä':'ae','ö':'oe','ü':'ue','ß':'ss','š':'s','č':'c','ć':'c','ž':'z','á':'a','à':'a','â':'a', | |
| 'é':'e','è':'e','ê':'e','ë':'e','í':'i','ó':'o','ò':'o','ô':'o','ñ':'n','ł':'l','ø':'o','ő':'o' | |
| }; | |
| function normalizeStr(s){ | |
| if(!s) return ''; | |
| let res = s.trim().toLowerCase(); | |
| // replace diacritics | |
| res = res.replace(/[^ -~]/g, ch => DIACRIT_MAP[ch] || ch); | |
| // remove invisible chars | |
| res = res.replace(/\s+/g, ''); | |
| return res; | |
| } | |
| function uniq(arr){ | |
| return Array.from(new Set(arr.filter(Boolean))); | |
| } | |
| function sampleWeighted(map){ | |
| // map: {key: weight} | |
| const keys = Object.keys(map); | |
| if(!keys.length) return null; | |
| const total = keys.reduce((a,k)=>a+ (map[k]||0),0); | |
| let r = Math.random()*total; | |
| for(const k of keys){ | |
| r -= (map[k]||0); | |
| if(r <= 0) return k; | |
| } | |
| return keys[ keys.length-1 ]; | |
| } | |
| /* ---------- State ---------- */ | |
| let analysisState = { | |
| totalLines:0, | |
| uniqueLocal:0, | |
| domainCounts:{}, | |
| patternCounts:{fn_dot_ln:0,fi_dot_ln:0,fnln:0,fn_digits:0,nick_digits:0,pure_nick:0,other:0}, | |
| suffixCounts:{}, | |
| nameCandidates:{first:{},last:{}}, | |
| domainMap:{} | |
| }; | |
| let customPatterns = [ | |
| {id: 1, name: 'first.last', template: '{first}.{last}', enabled: true}, | |
| {id: 2, name: 'firstlast', template: '{first}{last}', enabled: true}, | |
| {id: 3, name: 'first_digit', template: '{first}{digit}', enabled: true}, | |
| {id: 4, name: 'first.last.year', template: '{first}.{last}{year}', enabled: true} | |
| ]; | |
| let doneUsernames = new Set(); | |
| let lastGenerated = []; | |
| /* ---------- DOM refs ---------- */ | |
| const fileInput = document.getElementById('fileInput'); | |
| const doneFileInput = document.getElementById('doneFileInput'); | |
| const settingsFileInput = document.getElementById('settingsFileInput'); | |
| const analyzeBtn = document.getElementById('analyzeBtn'); | |
| const resetBtn = document.getElementById('resetBtn'); | |
| const patternList = document.getElementById('patternList'); | |
| const fileProgress = document.getElementById('fileProgress'); | |
| const statTotal = document.getElementById('statTotal'); | |
| const statUnique = document.getElementById('statUnique'); | |
| const statTopDomain = document.getElementById('statTopDomain'); | |
| const statPatterns = document.getElementById('statPatterns'); | |
| const optNormalize = document.getElementById('optNormalize'); | |
| const optExtractNames = document.getElementById('optExtractNames'); | |
| const doneCount = document.getElementById('doneCount'); | |
| const genBtn = document.getElementById('genBtn'); | |
| const previewBtn = document.getElementById('previewBtn'); | |
| const openSampleBtn = document.getElementById('openSampleBtn'); | |
| const genCountInput = document.getElementById('genCount'); | |
| const domainPreset = document.getElementById('domainPreset'); | |
| const customDomainContainer = document.getElementById('customDomainContainer'); | |
| const customDomain = document.getElementById('customDomain'); | |
| const sufCommon = document.getElementById('sufCommon'); | |
| const sufYears = document.getElementById('sufYears'); | |
| const firstPoolTA = document.getElementById('firstPool'); | |
| const lastPoolTA = document.getElementById('lastPool'); | |
| const outList = document.getElementById('outList'); | |
| const exportTxt = document.getElementById('exportTxt'); | |
| const exportCsv = document.getElementById('exportCsv'); | |
| const exportJson = document.getElementById('exportJson'); | |
| const exportSettings = document.getElementById('exportSettings'); | |
| const exportDone = document.getElementById('exportDone'); | |
| const analyzeProgress = fileProgress; | |
| const firstSeeds = document.getElementById('firstSeeds'); | |
| const lastSeeds = document.getElementById('lastSeeds'); | |
| const addPatternBtn = document.getElementById('addPatternBtn'); | |
| const customPatternsContainer = document.getElementById('customPatternsContainer'); | |
| const applyPresetsBtn = document.getElementById('applyPresetsBtn'); | |
| /* ---------- Custom Patterns UI ---------- */ | |
| function renderCustomPatterns() { | |
| customPatternsContainer.innerHTML = ''; | |
| customPatterns.forEach(pattern => { | |
| const patternEl = document.createElement('div'); | |
| patternEl.className = 'custom-pattern'; | |
| patternEl.innerHTML = ` | |
| <div class="pattern-header"> | |
| <div class="pattern-name">${pattern.name}</div> | |
| <div class="pattern-enabled"> | |
| <input type="checkbox" data-id="${pattern.id}" ${pattern.enabled ? 'checked' : ''}> | |
| <span class="small">Вкл</span> | |
| </div> | |
| </div> | |
| <div class="pattern-template">${pattern.template}</div> | |
| <div class="pattern-controls"> | |
| <button class="btn alt edit-pattern" data-id="${pattern.id}">Редактировать</button> | |
| <button class="btn alt danger remove-pattern" data-id="${pattern.id}">Удалить</button> | |
| </div> | |
| `; | |
| customPatternsContainer.appendChild(patternEl); | |
| }); | |
| // Add event listeners | |
| document.querySelectorAll('.edit-pattern').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const id = parseInt(e.target.dataset.id); | |
| editCustomPattern(id); | |
| }); | |
| }); | |
| document.querySelectorAll('.remove-pattern').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const id = parseInt(e.target.dataset.id); | |
| removeCustomPattern(id); | |
| }); | |
| }); | |
| document.querySelectorAll('.pattern-enabled input').forEach(checkbox => { | |
| checkbox.addEventListener('change', (e) => { | |
| const id = parseInt(e.target.dataset.id); | |
| toggleCustomPattern(id, e.target.checked); | |
| }); | |
| }); | |
| } | |
| function addCustomPattern() { | |
| const newPattern = { | |
| id: Date.now(), | |
| name: `pattern_${customPatterns.length + 1}`, | |
| template: '{first}.{last}', | |
| enabled: true | |
| }; | |
| customPatterns.push(newPattern); | |
| renderCustomPatterns(); | |
| editCustomPattern(newPattern.id); | |
| } | |
| function editCustomPattern(id) { | |
| const pattern = customPatterns.find(p => p.id === id); | |
| if (!pattern) return; | |
| const newName = prompt('Название шаблона:', pattern.name); | |
| if (newName === null) return; | |
| const newTemplate = prompt('Шаблон (переменные: {first} {last} {nick} {digit} {year}):', pattern.template); | |
| if (newTemplate === null) return; | |
| pattern.name = newName; | |
| pattern.template = newTemplate; | |
| renderCustomPatterns(); | |
| } | |
| function removeCustomPattern(id) { | |
| if (customPatterns.length <= 1) { | |
| alert('Нельзя удалить последний шаблон'); | |
| return; | |
| } | |
| customPatterns = customPatterns.filter(p => p.id !== id); | |
| renderCustomPatterns(); | |
| } | |
| function toggleCustomPattern(id, enabled) { | |
| const pattern = customPatterns.find(p => p.id === id); | |
| if (pattern) { | |
| pattern.enabled = enabled; | |
| } | |
| } | |
| /* ---------- Event Listeners ---------- */ | |
| domainPreset.addEventListener('change', () => { | |
| customDomainContainer.style.display = domainPreset.value === 'custom' ? 'block' : 'none'; | |
| }); | |
| addPatternBtn.addEventListener('click', addCustomPattern); | |
| applyPresetsBtn.addEventListener('click', () => { | |
| // Apply presets to pools | |
| if (firstSeeds.value && !firstPoolTA.value.trim()) { | |
| firstPoolTA.value = firstSeeds.value; | |
| } | |
| if (lastSeeds.value && !lastPoolTA.value.trim()) { | |
| lastPoolTA.value = lastSeeds.value; | |
| } | |
| alert('Предустановки применены к пулам имен и фамилий'); | |
| }); | |
| /* ---------- File parsing & analysis (chunked with worker) ---------- */ | |
| function resetAnalysis(){ | |
| analysisState = { | |
| totalLines:0, uniqueLocal:0, domainCounts:{}, | |
| patternCounts:{fn_dot_ln:0,fi_dot_ln:0,fnln:0,fn_digits:0,nick_digits:0,pure_nick:0,other:0}, | |
| suffixCounts:{}, nameCandidates:{first:{},last:{}}, domainMap:{} | |
| }; | |
| patternList.innerHTML = ''; | |
| statTotal.textContent = '0'; | |
| statUnique.textContent = '0'; | |
| statTopDomain.textContent = '—'; | |
| statPatterns.textContent = '—'; | |
| outList.textContent = ''; | |
| lastGenerated = []; | |
| fileProgress.style.width = '0%'; | |
| } | |
| resetBtn.addEventListener('click', ()=>{ | |
| resetAnalysis(); | |
| fileInput.value=''; | |
| doneFileInput.value=''; | |
| settingsFileInput.value=''; | |
| doneUsernames.clear(); | |
| doneCount.textContent = 'Загружено: 0 логинов'; | |
| }); | |
| analyzeBtn.addEventListener('click', ()=>{ | |
| const file = fileInput.files && fileInput.files[0]; | |
| if(!file){ | |
| alert('Выберите .txt файл с логинами первым.'); | |
| return; | |
| } | |
| resetAnalysis(); | |
| analyzeFileChunked(file); | |
| }); | |
| function analyzeFileChunked(file){ | |
| const workerCode = `self.onmessage = function(ev){ | |
| const {action, chunk, eof} = ev.data; | |
| if(action === 'analyzeChunk'){ | |
| // chunk: string (portion of file) | |
| // We'll split by newlines, extract emails and emit local-parts + domains + pattern counts + suffix detection | |
| const lines = chunk.split(/\\r?\\n/).map(l=>l.trim()).filter(Boolean); | |
| const domainCounts = {}; const localSet = new Set(); | |
| const patternCounts = {fn_dot_ln:0, fi_dot_ln:0, fnln:0, fn_digits:0, nick_digits:0, pure_nick:0, other:0}; | |
| const suffixCounts = {}; | |
| const nameCandidates = {first:{},last:{}}; | |
| const domainMap = {}; | |
| for(const raw of lines){ | |
| try{ | |
| const lower = raw.toLowerCase(); | |
| if(!lower.includes('@')) continue; | |
| const [local, domain] = lower.split('@'); | |
| if(!local) continue; | |
| localSet.add(local); | |
| domainCounts[domain] = (domainCounts[domain] || 0) + 1; | |
| // pattern detection (simple heuristics) | |
| // fn.ln or fn.lnNN | |
| if(/^[a-z]+\\.[a-z]+\\d*$/.test(local)){ | |
| patternCounts.fn_dot_ln++; | |
| const parts = local.split('.'); | |
| if(parts.length>=2){ | |
| const fn = parts[0].replace(/\\d+$/,''); const ln = parts.slice(1).join('.').replace(/\\d+$/,''); | |
| if(fn) nameCandidates.first[fn] = (nameCandidates.first[fn]||0)+1; | |
| if(ln) nameCandidates.last[ln] = (nameCandidates.last[ln]||0)+1; | |
| } | |
| } else if(/^[a-z]\\.[a-z]+\\d*$/.test(local)){ | |
| patternCounts.fi_dot_ln++; | |
| } else if(/^[a-z]+[a-z]+\\d*$/.test(local) && /[0-9]/.test(local) && /[a-z]/.test(local)){ | |
| // letters + digits mixed | |
| // differentiate nick_digits vs fn_digits heuristics by presence of dot or underscore earlier (we checked) | |
| patternCounts.fn_digits++; | |
| } else if(/^[a-z]+\\d+$/.test(local)){ | |
| patternCounts.nick_digits++; | |
| } else if(/^[a-z]+$/.test(local)){ | |
| patternCounts.pure_nick++; | |
| // candidate could be either first or last; increment in both maps for possible extraction | |
| nameCandidates.first[local] = (nameCandidates.first[local]||0)+1; | |
| nameCandidates.last[local] = (nameCandidates.last[local]||0)+1; | |
| } else { | |
| patternCounts.other++; | |
| } | |
| // suffix extraction (numbers at end) | |
| const m = local.match(/(\\d{1,8})$/); | |
| if(m){ | |
| const suf = m[1]; | |
| suffixCounts[suf] = (suffixCounts[suf]||0)+1; | |
| } | |
| domainMap[domain] = (domainMap[domain]||0)+1; | |
| }catch(e){/*ignore per-line errors*/ } | |
| } | |
| // respond with partial results | |
| self.postMessage({action:'chunkResult',domainCounts,patternCounts,localCount: localSet.size,suffixCounts,nameCandidates,domainMap}); | |
| if(eof) self.postMessage({action:'done'}); | |
| } | |
| };`; | |
| const workerBlob = new Blob([workerCode], {type:'application/javascript'}); | |
| const workerUrl = URL.createObjectURL(workerBlob); | |
| const worker = new Worker(workerUrl); | |
| const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB chunk | |
| let offset = 0; | |
| let partial = ''; | |
| const reader = new FileReader(); | |
| worker.onmessage = function(ev){ | |
| const data = ev.data; | |
| if(data.action === 'chunkResult'){ | |
| // merge into analysisState | |
| mergeCounts(analysisState, data); | |
| updateStatsUI(); | |
| } else if(data.action === 'done'){ | |
| // finalize | |
| finalizeAnalysis(); | |
| worker.terminate(); | |
| URL.revokeObjectURL(workerUrl); | |
| } | |
| }; | |
| reader.onerror = err => { | |
| alert('Ошибка чтения файла: '+ err); | |
| worker.terminate(); | |
| URL.revokeObjectURL(workerUrl); | |
| }; | |
| reader.onload = function(e){ | |
| try{ | |
| let text = e.target.result; | |
| // prepend partial leftover | |
| text = partial + text; | |
| // try to keep last line partial if file continues | |
| const lastNewline = text.lastIndexOf('\n'); | |
| let chunkToSend = text; | |
| if(lastNewline !== -1 && offset + CHUNK_SIZE < file.size){ | |
| chunkToSend = text.slice(0, lastNewline+1); | |
| partial = text.slice(lastNewline+1); | |
| } else { // final chunk or small file | |
| partial = ''; | |
| } | |
| const eof = (offset + CHUNK_SIZE) >= file.size; | |
| worker.postMessage({action:'analyzeChunk', chunk:chunkToSend, eof}); | |
| offset += CHUNK_SIZE; | |
| // update progress | |
| const pct = Math.min(100, Math.round((offset / file.size) * 100)); | |
| fileProgress.style.width = pct + '%'; | |
| if(offset < file.size){ | |
| readSlice(); | |
| } else { | |
| // done reading | |
| } | |
| }catch(err){ | |
| console.error(err); | |
| worker.terminate(); | |
| URL.revokeObjectURL(workerUrl); | |
| alert('Ошибка обработки чанка: '+err); | |
| } | |
| }; | |
| function readSlice(){ | |
| const slice = file.slice(offset, offset + CHUNK_SIZE); | |
| reader.readAsText(slice); | |
| } | |
| // start | |
| readSlice(); | |
| } | |
| /* merge worker results into analysisState */ | |
| function mergeCounts(state, data){ | |
| // domains | |
| for(const d in data.domainCounts){ | |
| state.domainCounts[d] = (state.domainCounts[d] || 0) + data.domainCounts[d]; | |
| } | |
| // patterns | |
| for(const k in state.patternCounts){ | |
| state.patternCounts[k] = (state.patternCounts[k] || 0) + (data.patternCounts[k] || 0); | |
| } | |
| // suffixes | |
| for(const s in data.suffixCounts){ | |
| state.suffixCounts[s] = (state.suffixCounts[s] || 0) + data.suffixCounts[s]; | |
| } | |
| // names | |
| ['first','last'].forEach(kind=>{ | |
| const cand = data.nameCandidates?.[kind] || {}; | |
| for(const nm in cand){ | |
| state.nameCandidates[kind][nm] = (state.nameCandidates[kind][nm]||0) + cand[nm]; | |
| } | |
| }); | |
| // unique local count approximation (we sum partial unique counts, but we will recompute accurately later if needed) | |
| state.uniqueLocal += data.localCount || 0; | |
| // domainMap | |
| for(const d in data.domainMap){ | |
| state.domainMap[d] = (state.domainMap[d] || 0) + data.domainMap[d]; | |
| } | |
| } | |
| /* update compact UI */ | |
| function updateStatsUI(){ | |
| const total = Object.values(analysisState.domainMap).reduce((a,b)=>a+b,0); | |
| statTotal.textContent = total || '0'; | |
| statUnique.textContent = analysisState.uniqueLocal || '0'; | |
| // top domain | |
| const domainEntries = Object.entries(analysisState.domainCounts||{}).sort((a,b)=>b[1]-a[1]); | |
| statTopDomain.textContent = domainEntries.length ? `${domainEntries[0][0]} (${domainEntries[0][1]})` : '—'; | |
| // patterns summary | |
| const pc = analysisState.patternCounts; | |
| const sumP = Object.values(pc).reduce((a,b)=>a+b,0) || 0; | |
| statPatterns.textContent = sumP ? Object.entries(pc).map(([k,v])=>`${k}:${v}`).join(' | ') : '—'; | |
| // render pattern list interactive | |
| renderPatternList(pc); | |
| } | |
| /* render interactive pattern list with checkboxes and weight sliders */ | |
| function renderPatternList(patternCounts){ | |
| patternList.innerHTML = ''; | |
| const total = Object.values(patternCounts).reduce((a,b)=>a+b,0) || 1; | |
| for(const [k,v] of Object.entries(patternCounts)){ | |
| const pct = Math.round((v/total)*100); | |
| const item = document.createElement('div'); | |
| item.className = 'pattern-item'; | |
| item.innerHTML = ` | |
| <div style="display:flex;gap:10px;align-items:center"> | |
| <input type="checkbox" data-pattern="${k}" checked /> | |
| <div style="min-width:120px"><strong>${k}</strong></div> | |
| <div class="small muted">${v} hits</div> | |
| </div> | |
| <div style="width:40%"> | |
| <div style="height:8px;background:rgba(255,255,255,0.03);border-radius:6px;overflow:hidden"> | |
| <div class="bar" style="width:${pct}%;"></div> | |
| </div> | |
| </div> | |
| `; | |
| patternList.appendChild(item); | |
| } | |
| } | |
| /* finalize analysis: compute derived pools */ | |
| function finalizeAnalysis(){ | |
| // compute normalized name pools from analysisState.nameCandidates (top N) | |
| const firsts = Object.entries(analysisState.nameCandidates.first || {}).sort((a,b)=>b[1]-a[1]).slice(0,200).map(x=>normalizeStr(x[0])); | |
| const lasts = Object.entries(analysisState.nameCandidates.last || {}).sort((a,b)=>b[1]-a[1]).slice(0,200).map(x=>normalizeStr(x[0])); | |
| // put into textareas only if they are empty (user may override) | |
| if(!firstPoolTA.value.trim()){ | |
| firstPoolTA.value = firsts.join('\n'); | |
| } | |
| if(!lastPoolTA.value.trim()){ | |
| lastPoolTA.value = lasts.join('\n'); | |
| } | |
| // preset suffix common list | |
| const topSuffixes = Object.entries(analysisState.suffixCounts || {}).sort((a,b)=>b[1]-a[1]).slice(0,20).map(x=>x[0]); | |
| sufCommon.value = topSuffixes.slice(0,12).join(','); | |
| updateStatsUI(); | |
| renderCustomPatterns(); | |
| alert('Анализ завершён. Проверьте автоматически заполненные пулы имён и фамилий. Отредактируйте при необходимости и нажмите "Генерировать".'); | |
| } | |
| /* ---------- Done file handling ---------- */ | |
| doneFileInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (!file || !file.name.endsWith('.txt')) { | |
| alert('Пожалуйста, выберите файл с расширением .txt'); | |
| return; | |
| } | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| try { | |
| const content = event.target.result; | |
| const lines = content.split(/[\r\n]+/).map(line => line.trim()).filter(Boolean); | |
| doneUsernames = new Set(lines); | |
| doneCount.textContent = `Загружено: ${doneUsernames.size} логинов`; | |
| alert(`Загружено ${doneUsernames.size} уже сгенерированных логинов для пропуска`); | |
| } catch (err) { | |
| alert('Ошибка при чтении файла done.txt: ' + err.message); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| }); | |
| /* ---------- Settings import/export ---------- */ | |
| settingsFileInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (!file || !file.name.endsWith('.json')) { | |
| alert('Пожалуйста, выберите файл с расширением .json'); | |
| return; | |
| } | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| try { | |
| const content = event.target.result; | |
| const parsedSettings = JSON.parse(content); | |
| if (parsedSettings.settings) { | |
| // Restore settings | |
| const settings = parsedSettings.settings; | |
| optNormalize.checked = settings.normalize; | |
| optExtractNames.checked = settings.extractNames; | |
| firstSeeds.value = settings.firstSeeds || ''; | |
| lastSeeds.value = settings.lastSeeds || ''; | |
| genCountInput.value = settings.genCount || 200; | |
| domainPreset.value = settings.domainPreset || 'auto'; | |
| sufCommon.value = settings.sufCommon || ''; | |
| sufYears.value = settings.sufYears || ''; | |
| firstPoolTA.value = settings.firstPool || ''; | |
| lastPoolTA.value = settings.lastPool || ''; | |
| customDomain.value = settings.customDomain || ''; | |
| // Restore analysis state if available | |
| if (parsedSettings.analysisState) { | |
| analysisState = parsedSettings.analysisState; | |
| updateStatsUI(); | |
| } | |
| // Restore custom patterns | |
| if (parsedSettings.customPatterns) { | |
| customPatterns = parsedSettings.customPatterns; | |
| renderCustomPatterns(); | |
| } | |
| // Restore generated usernames | |
| if (parsedSettings.generatedUsernames) { | |
| lastGenerated = parsedSettings.generatedUsernames; | |
| renderOutput(lastGenerated); | |
| } | |
| // Restore done usernames | |
| if (parsedSettings.doneUsernames) { | |
| doneUsernames = new Set(parsedSettings.doneUsernames); | |
| doneCount.textContent = `Загружено: ${doneUsernames.size} логинов`; | |
| } | |
| alert('Настройки успешно восстановлены!'); | |
| } else { | |
| alert('Неверный формат файла настроек'); | |
| } | |
| } catch (err) { | |
| alert('Ошибка при чтении файла настроек: ' + err.message); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| }); | |
| /* ---------- Generation logic ---------- */ | |
| function getSelectedPatterns(){ | |
| return Array.from(patternList.querySelectorAll('input[type="checkbox"]:checked')).map(cb=>cb.dataset.pattern); | |
| } | |
| function getDomainDistribution(){ | |
| // If custom domain is set, use it | |
| if (domainPreset.value === 'custom' && customDomain.value.trim()) { | |
| const domain = customDomain.value.trim().toLowerCase(); | |
| return { [domain]: 1 }; | |
| } | |
| const preset = domainPreset.value; | |
| const domainCounts = analysisState.domainCounts || {}; | |
| if(preset === 'auto'){ | |
| return normalizeDistribution(domainCounts); | |
| } | |
| // define some domain groups | |
| const local = ['gmx.at','aon.at','chello.at','liwest.at','inode.at','student.uibk.ac.at','proton.me','protonmail.com','medundmed.at','drei.at','tmo.at']; | |
| const global = ['gmail.com','yahoo.com','outlook.com','hotmail.com','live.com','googlemail.com','msn.com','ymail.com']; | |
| const dist = {}; | |
| if(preset === 'local'){ | |
| local.forEach(d=>dist[d]=1); | |
| } else if(preset === 'global'){ | |
| global.forEach(d=>dist[d]=1); | |
| } | |
| return normalizeDistribution(dist); | |
| } | |
| function normalizeDistribution(map){ | |
| const m = {}; | |
| const keys = Object.keys(map); | |
| if(!keys.length) return {'gmail.com':1}; | |
| let total = 0; | |
| for(const k of keys){ m[k] = Number(map[k]||0); total += m[k]; } | |
| if(total === 0){ | |
| // fallback: equal weights | |
| keys.forEach(k=>m[k]=1); | |
| total = keys.length; | |
| } | |
| // return normalized weights (not necessary but keep numbers) | |
| return m; | |
| } | |
| /* parse pools */ | |
| function parsePool(text){ | |
| if(!text) return []; | |
| const arr = text.split(/[\n,;]+/).map(s=>normalizeStr(s)).filter(Boolean); | |
| return uniq(arr); | |
| } | |
| /* build pattern application functions */ | |
| function buildGenerators(selectedPatterns, firstPool, lastPool, nickPool, suffixList, yearRange, domainWeights, customPatterns){ | |
| const gens = []; | |
| // helper small funcs | |
| const rnd = arr => arr[Math.floor(Math.random()*arr.length)]; | |
| const pickDomain = ()=> sampleWeighted(domainWeights) || 'gmail.com'; | |
| const pickSuffix = ()=> suffixList.length ? suffixList[Math.floor(Math.random()*suffixList.length)] : ''; | |
| const pickYearSuffix = ()=>{ | |
| if(!yearRange) return ''; | |
| const [a,b] = yearRange; | |
| const y = a + Math.floor(Math.random()*(b-a+1)); | |
| return String(y); | |
| }; | |
| const maybeNum = ()=>{ | |
| if(Math.random()<0.45){ | |
| if(Math.random()<0.5) return pickSuffix(); | |
| return pickYearSuffix(); | |
| } | |
| return ''; | |
| }; | |
| // Add custom pattern generators | |
| const enabledCustomPatterns = customPatterns.filter(p => p.enabled); | |
| enabledCustomPatterns.forEach(pattern => { | |
| gens.push(()=>{ | |
| const first = rnd(firstPool) || 'john'; | |
| const last = rnd(lastPool) || 'doe'; | |
| const nick = rnd(nickPool) || first; | |
| const digit = Math.floor(Math.random() * 1000); | |
| const year = pickYearSuffix() || '1990'; | |
| let local = pattern.template | |
| .replace(/{first}/g, first) | |
| .replace(/{last}/g, last) | |
| .replace(/{nick}/g, nick) | |
| .replace(/{digit}/g, digit.toString()) | |
| .replace(/{year}/g, year); | |
| // Add optional suffix | |
| if(Math.random()<0.3){ | |
| local += maybeNum(); | |
| } | |
| return `${local}@${pickDomain()}`; | |
| }); | |
| }); | |
| for(const p of selectedPatterns){ | |
| if(p === 'fn_dot_ln'){ | |
| gens.push(()=>{ | |
| const f = rnd(firstPool); const l = rnd(lastPool); | |
| let local = `${f}.${l}`; | |
| if(Math.random()<0.4){ local += maybeNum(); } | |
| return `${local}@${pickDomain()}`; | |
| }); | |
| } else if(p === 'fi_dot_ln'){ | |
| gens.push(()=>{ | |
| const f = rnd(firstPool); const l = rnd(lastPool); | |
| let local = `${f[0]}.${l}`; | |
| if(Math.random()<0.3){ local += maybeNum(); } | |
| return `${local}@${pickDomain()}`; | |
| }); | |
| } else if(p === 'fnln'){ | |
| gens.push(()=>{ | |
| const f = rnd(firstPool); const l = rnd(lastPool); | |
| let local = `${f}${l}`; | |
| if(Math.random()<0.35){ local += maybeNum(); } | |
| return `${local}@${pickDomain()}`; | |
| }); | |
| } else if(p === 'fn_digits' || p === 'nick_digits'){ | |
| gens.push(()=>{ | |
| const chooseNick = Math.random()<0.5; | |
| const base = chooseNick ? (rnd(nickPool)||rnd(firstPool)||'user') : (rnd(firstPool)+ (Math.random()<0.3?'.':'' ) + (rnd(lastPool)||'')); | |
| const su = maybeNum() || pickSuffix(); | |
| const local = base + su; | |
| return `${local}@${pickDomain()}`; | |
| }); | |
| } else if(p === 'pure_nick'){ | |
| gens.push(()=>{ | |
| const base = rnd(nickPool) || rnd(firstPool) || 'user'; | |
| const local = (Math.random()<0.35) ? (base + maybeNum()) : base; | |
| return `${local}@${pickDomain()}`; | |
| }); | |
| } else { | |
| // fallback generic | |
| gens.push(()=>{ | |
| const f = rnd(firstPool) || 'john'; const l = rnd(lastPool) || 'doe'; | |
| let local = `${f}.${l}`; | |
| if(Math.random()<0.4) local += maybeNum(); | |
| return `${local}@${pickDomain()}`; | |
| }); | |
| } | |
| } | |
| // ensure at least one generator | |
| if(gens.length === 0 && enabledCustomPatterns.length === 0){ | |
| gens.push(()=>{ | |
| const f = rnd(firstPool) || 'john'; const l = rnd(lastPool) || 'doe'; | |
| return `${f}.${l}@gmail.com`; | |
| }); | |
| } | |
| return gens; | |
| } | |
| /* parse year range string like "1960-2005" */ | |
| function parseYearRange(s){ | |
| if(!s) return null; | |
| const m = s.match(/(\d{3,4})\s*-\s*(\d{3,4})/); | |
| if(m){ | |
| const a = Math.max(1900, Number(m[1])); | |
| const b = Math.min(2100, Number(m[2])); | |
| if(a<=b) return [a,b]; | |
| } | |
| return null; | |
| } | |
| /* generator driver */ | |
| function generateList(count, options = {}){ | |
| const selectedPatterns = getSelectedPatterns(); | |
| const enabledCustomPatterns = customPatterns.filter(p => p.enabled); | |
| if(selectedPatterns.length === 0 && enabledCustomPatterns.length === 0){ | |
| alert('Выберите хотя бы один паттерн для генерации.'); | |
| return []; | |
| } | |
| const firstPool = parsePool(firstPoolTA.value) .length ? parsePool(firstPoolTA.value) : parsePool(firstSeeds.value); | |
| const lastPool = parsePool(lastPoolTA.value) .length ? parsePool(lastPoolTA.value) : parsePool(lastSeeds.value); | |
| // fallback: if pools empty, derive from analysis top candidates | |
| const fPool = firstPool.length ? firstPool : Object.keys(analysisState.nameCandidates.first || {}).slice(0,200).map(k=>normalizeStr(k)); | |
| const lPool = lastPool.length ? lastPool : Object.keys(analysisState.nameCandidates.last || {}).slice(0,200).map(k=>normalizeStr(k)); | |
| const domainWeights = getDomainDistribution(); | |
| const suffixList = sufCommon.value ? uniq(sufCommon.value.split(/[\n,;]+/).map(s=>s.trim()).filter(Boolean)) : Object.keys(analysisState.suffixCounts||{}).slice(0,20); | |
| const yearRange = parseYearRange(sufYears.value) || [1970,2005]; | |
| const gens = buildGenerators(selectedPatterns, fPool, lPool, fPool, suffixList, yearRange, domainWeights, customPatterns); | |
| const out = new Set(); | |
| // generation loop with dedup and safety cap | |
| const CAP = Math.max(count*10, 2000); // attempts cap to avoid infinite loops | |
| let attempts = 0; | |
| while(out.size < count && attempts < CAP){ | |
| attempts++; | |
| const genFunc = gens[Math.floor(Math.random()*gens.length)]; | |
| try{ | |
| const val = genFunc(); | |
| if(val && typeof val === 'string' && !doneUsernames.has(val)){ | |
| out.add(val); | |
| } | |
| }catch(e){ console.error('generator error', e); } | |
| } | |
| lastGenerated = Array.from(out); | |
| // show results | |
| renderOutput(lastGenerated); | |
| return lastGenerated; | |
| } | |
| /* email normalization: keep local part allowed characters and domain as is */ | |
| function normalizeStrEmail(email){ | |
| let parts = String(email).trim().toLowerCase().split('@'); | |
| if(parts.length < 2) return email; | |
| let local = parts.slice(0,parts.length-1).join('@'); // in case local had @ (rare) | |
| const domain = parts[parts.length-1].trim(); | |
| // replace diacritics in local | |
| local = local.replace(/[^ -~]/g, ch => DIACRIT_MAP[ch] || ch); | |
| // allowed chars for local: a-z0-9._-+ (we keep plus signs too as they appear in data) | |
| local = local.replace(/[^a-z0-9._\-+]/g,''); | |
| // avoid leading/trailing dot | |
| local = local.replace(/^\.*|\.*$/g,''); | |
| return local + '@' + domain; | |
| } | |
| /* render output */ | |
| function renderOutput(list){ | |
| outList.innerText = list.join('\n'); | |
| } | |
| /* ---------- UI events ---------- */ | |
| previewBtn.addEventListener('click', ()=>{ | |
| const res = generateList(Math.min(25, Number(genCountInput.value) || 25)); | |
| alert('Предпросмотр: ' + res.length + ' записей сгенерировано (показаны в списке).'); | |
| }); | |
| genBtn.addEventListener('click', ()=>{ | |
| const cnt = Math.max(1, Number(genCountInput.value) || 100); | |
| generateList(cnt); | |
| alert('Генерация завершена: ' + lastGenerated.length + ' уникальных записей.'); | |
| }); | |
| openSampleBtn.addEventListener('click', ()=>{ | |
| // show sample from analysis if available | |
| const sample = Object.keys(analysisState.domainMap||{}).slice(0,10).map(d=>d+': '+(analysisState.domainMap[d]||0)).join('\n'); | |
| alert('Top domains samples:\n' + sample); | |
| }); | |
| /* exports */ | |
| function download(filename, text){ | |
| const blob = new Blob([text], {type:'text/plain;charset=utf-8'}); | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = filename; | |
| document.body.appendChild(a); a.click(); | |
| setTimeout(()=>{ URL.revokeObjectURL(a.href); a.remove(); }, 100); | |
| } | |
| function getTimestamp() { | |
| return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); | |
| } | |
| function getFileNameBase() { | |
| const file = fileInput.files && fileInput.files[0]; | |
| const timestamp = getTimestamp(); | |
| return file ? `${file.name.replace('.txt', '')}_${timestamp}` : `usernames_${timestamp}`; | |
| } | |
| exportTxt.addEventListener('click', ()=> { | |
| if(!lastGenerated.length){ alert('Нет сгенерированных данных.'); return; } | |
| const fileNameBase = getFileNameBase(); | |
| download(`${fileNameBase}_${lastGenerated.length}-lines.txt`, lastGenerated.join('\n')); | |
| }); | |
| exportCsv.addEventListener('click', ()=> { | |
| if(!lastGenerated.length){ alert('Нет сгенерированных данных.'); return; } | |
| const fileNameBase = getFileNameBase(); | |
| // simple CSV with column email | |
| const csv = 'email\n' + lastGenerated.map(e=>`"${e.replace(/"/g,'""')}"`).join('\n'); | |
| download(`${fileNameBase}_${lastGenerated.length}-lines.csv`, csv); | |
| }); | |
| exportJson.addEventListener('click', ()=> { | |
| if(!lastGenerated.length){ alert('Нет сгенерированных данных.'); return; } | |
| const fileNameBase = getFileNameBase(); | |
| download(`${fileNameBase}_${lastGenerated.length}-lines.json`, JSON.stringify(lastGenerated, null, 2)); | |
| }); | |
| exportSettings.addEventListener('click', ()=> { | |
| const fileNameBase = getFileNameBase(); | |
| const settingsData = { | |
| settings: { | |
| normalize: optNormalize.checked, | |
| extractNames: optExtractNames.checked, | |
| firstSeeds: firstSeeds.value, | |
| lastSeeds: lastSeeds.value, | |
| genCount: genCountInput.value, | |
| domainPreset: domainPreset.value, | |
| sufCommon: sufCommon.value, | |
| sufYears: sufYears.value, | |
| firstPool: firstPoolTA.value, | |
| lastPool: lastPoolTA.value, | |
| customDomain: customDomain.value | |
| }, | |
| analysisState: analysisState, | |
| customPatterns: customPatterns, | |
| generatedUsernames: lastGenerated, | |
| doneUsernames: Array.from(doneUsernames), | |
| timestamp: new Date().toISOString() | |
| }; | |
| download(`${fileNameBase}_settings.json`, JSON.stringify(settingsData, null, 2)); | |
| }); | |
| exportDone.addEventListener('click', ()=> { | |
| if(!lastGenerated.length){ alert('Нет сгенерированных данных.'); return; } | |
| const fileNameBase = getFileNameBase(); | |
| download(`${fileNameBase}_done.txt`, lastGenerated.join('\n')); | |
| }); | |
| /* ---------- Helpers for UI and sanity ---------- */ | |
| function safeParseInt(v, d){ const n = parseInt(v,10); return isNaN(n)? d : n; } | |
| /* ---------- Initialize small defaults ---------- */ | |
| (function initDefaults(){ | |
| // prefill sufYears example | |
| sufYears.placeholder = 'Пример: 1960-2005'; | |
| sufCommon.placeholder = 'Например: 007,123,84,2005'; | |
| renderCustomPatterns(); | |
| // Set custom domain as default | |
| domainPreset.value = 'custom'; | |
| customDomain.value = 'gmx.de'; | |
| })(); | |
| /* ---------- End of script ---------- */ | |
| </script> | |
| </body> | |
| </html> |