generator / index.html
enotkrutoy's picture
Update index.html
5226d85 verified
<!DOCTYPE html>
<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>