enotkrutoy commited on
Commit
13b1c12
·
verified ·
1 Parent(s): ed87e78

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +844 -478
index.html CHANGED
@@ -1,536 +1,902 @@
1
- <!DOCTYPE html>
2
  <html lang="ru">
3
  <head>
4
- <meta charset="UTF-8">
5
- <title>Advanced Username Generator v3.0 [DYNAMIC RED TEAM]</title>
6
- <script src="https://cdn.jsdelivr.net/npm/seedrandom@3.0.5/seedrandom.min.js"></script>
7
  <style>
8
- body {
9
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
10
- color: #e6e6e6;
11
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
12
- padding: 20px;
13
- min-height: 100vh;
14
- margin: 0;
15
- }
16
- .container {
17
- max-width: 800px;
18
- margin: 0 auto;
19
- background: rgba(30, 30, 46, 0.8);
20
- border-radius: 15px;
21
- padding: 30px;
22
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
23
- backdrop-filter: blur(10px);
24
- border: 1px solid rgba(255, 255, 255, 0.1);
25
- }
26
- h2 {
27
- text-align: center;
28
- color: #4cc9f0;
29
- margin-bottom: 30px;
30
- font-size: 28px;
31
- text-shadow: 0 0 10px rgba(76, 201, 240, 0.3);
32
- }
33
- .label {
34
- display: block;
35
- margin: 15px 0 5px;
36
- font-weight: bold;
37
- color: #f72585;
38
- }
39
- input, textarea, button, select {
40
- width: 100%;
41
- margin: 8px 0;
42
- padding: 12px;
43
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
44
- border-radius: 8px;
45
- border: 1px solid rgba(255, 255, 255, 0.2);
46
- background: rgba(20, 20, 35, 0.7);
47
- color: #fff;
48
- box-sizing: border-box;
49
- transition: all 0.3s ease;
50
- }
51
- input:focus, textarea:focus, select:focus {
52
- outline: none;
53
- border-color: #4361ee;
54
- box-shadow: 0 0 10px rgba(67, 97, 238, 0.5);
55
- }
56
- button {
57
- cursor: pointer;
58
- background: linear-gradient(45deg, #4361ee, #3a0ca3);
59
- color: #fff;
60
- border: none;
61
- font-weight: bold;
62
- font-size: 16px;
63
- padding: 15px;
64
- margin-top: 20px;
65
- transition: all 0.3s ease;
66
- text-transform: uppercase;
67
- letter-spacing: 1px;
68
- }
69
- button:hover {
70
- background: linear-gradient(45deg, #3a0ca3, #4361ee);
71
- transform: translateY(-2px);
72
- box-shadow: 0 5px 15px rgba(67, 97, 238, 0.4);
73
- }
74
- button:active {
75
- transform: translateY(0);
76
- }
77
- textarea {
78
- min-height: 200px;
79
- resize: vertical;
80
- font-family: monospace;
81
- }
82
- .file-input-wrapper {
83
- position: relative;
84
- overflow: hidden;
85
- display: inline-block;
86
- width: 100%;
87
- }
88
- .file-input-wrapper input[type=file] {
89
- position: absolute;
90
- left: 0;
91
- top: 0;
92
- opacity: 0;
93
- cursor: pointer;
94
- width: 100%;
95
- height: 100%;
96
- }
97
- .file-input-button {
98
- display: block;
99
- padding: 12px;
100
- background: rgba(20, 20, 35, 0.7);
101
- border: 1px dashed rgba(255, 255, 255, 0.3);
102
- border-radius: 8px;
103
- text-align: center;
104
- color: #4cc9f0;
105
- transition: all 0.3s ease;
106
- }
107
- .file-input-button:hover {
108
- background: rgba(30, 30, 46, 0.9);
109
- border-color: #4361ee;
110
- }
111
- .pattern-info {
112
- background: rgba(20, 20, 35, 0.5);
113
- border-radius: 8px;
114
- padding: 15px;
115
- margin: 20px 0;
116
- border-left: 4px solid #f72585;
117
- display: none;
118
- }
119
- .pattern-list {
120
- display: grid;
121
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
122
- gap: 10px;
123
- margin-top: 10px;
124
- }
125
- .pattern-item {
126
- background: rgba(67, 97, 238, 0.2);
127
- padding: 8px;
128
- border-radius: 5px;
129
- font-size: 12px;
130
- white-space: nowrap;
131
- overflow: hidden;
132
- text-overflow: ellipsis;
133
- }
134
- .footer {
135
- text-align: center;
136
- margin-top: 30px;
137
- color: #7b7b9d;
138
- font-size: 14px;
139
- }
140
- .domain-section {
141
- margin: 20px 0;
142
- }
143
- .domain-options {
144
- display: flex;
145
- gap: 10px;
146
- margin: 10px 0;
147
- }
148
- .domain-option {
149
- flex: 1;
150
- text-align: center;
151
- padding: 10px;
152
- background: rgba(20, 20, 35, 0.7);
153
- border-radius: 8px;
154
- cursor: pointer;
155
- border: 2px solid transparent;
156
- transition: all 0.3s ease;
157
- }
158
- .domain-option:hover {
159
- border-color: #4361ee;
160
- }
161
- .domain-option.active {
162
- border-color: #f72585;
163
- background: rgba(247, 37, 133, 0.2);
164
- }
165
- .custom-domains {
166
- margin-top: 15px;
167
- }
168
- .custom-domains textarea {
169
- min-height: 100px;
170
- }
171
- .export-buttons {
172
- display: flex;
173
- gap: 10px;
174
- margin: 15px 0;
175
- }
176
  </style>
177
  </head>
178
  <body>
179
- <div class="container">
180
- <h2>Генератор логинов v3.0 [DYNAMIC RED TEAM]</h2>
181
-
182
- <div class="pattern-info" id="patternInfo">
183
- <h3>Выявленные паттерны (динамически из файла)</h3>
184
- <div class="pattern-list" id="patternList"></div>
185
- </div>
186
-
187
- <label class="label">Файл с логинами (.txt):</label>
188
- <div class="file-input-wrapper">
189
- <div class="file-input-button" id="fileInputLabel">Выберите файл</div>
190
- <input type="file" id="fileInput">
191
- </div>
192
-
193
- <label class="label">Имена ерез запятую):</label>
194
- <input type="text" id="firstNames" value="daniel,claudia,kevin,anna,roman,алексей,мария,иван,олег,екатерина">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
 
196
- <label class="label">Фамилии (через запятую):</label>
197
- <input type="text" id="lastNames" value="pirker,berghoffer,rohrer,schaefer,medved,иванов,петров,смирнов,козлов,васильева">
198
-
199
- <label class="label">Количество:</label>
200
- <input type="number" id="count" value="50" min="1" max="5000">
201
-
202
- <label class="label">Seed (для повторяемости):</label>
203
- <input type="text" id="seed" placeholder="Оставьте пустым для случайного">
204
 
205
- <label><input type="checkbox" id="stealthMode"> Stealth Mode</label>
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
- <div class="domain-section">
208
- <label class="label">Домены:</label>
209
- <div class="domain-options">
210
- <div class="domain-option active" data-type="default">По умолчанию</div>
211
- <div class="domain-option" data-type="custom"ользовательские</div>
 
 
 
 
 
 
 
 
212
  </div>
213
-
214
- <div class="custom-domains" id="customDomainsSection" style="display: none;">
215
- <label class="label">Введите домены (по одному на строку):</label>
216
- <textarea id="customDomains" placeholder="target.com&#10;corp.target.com">gmail.com
217
- yandex.ru
218
- mail.ru
219
- outlook.com</textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  </div>
221
  </div>
222
-
223
- <button onclick="generate()">Сгенерировать</button>
224
- <div class="export-buttons">
225
- <button onclick="exportCSV()">Скачать CSV</button>
226
- <button onclick="exportJSON()">Скачать JSON</button>
227
- <button onclick="clearOutput()">Очистить вывод</button>
228
- </div>
229
-
230
- <label class="label">Результат:</label>
231
- <textarea id="output" placeholder="Сгенерированные логины появятся здесь..."></textarea>
232
-
233
- <div class="footer">
234
- v3.0 — 100% динамический анализ. Никаких шаблонов. Только данные из файла.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  </div>
 
 
 
 
236
  </div>
237
 
238
  <script>
239
- let dynamicPatterns = []; // { structure: [...], count: N, examples: [...] }
240
- let allSeparators = new Set();
241
- let allTokenStats = { words: [], numbers: [], mixed: [] };
242
- let defaultDomains = ["gmail.com","yandex.ru","mail.ru","outlook.com","yahoo.com","hotmail.com","icloud.com","protonmail.com"];
243
- let currentDomainType = "default";
244
-
245
- function analyzeDynamicPatterns(lines) {
246
- dynamicPatterns = [];
247
- allSeparators.clear();
248
- allTokenStats = { words: [], numbers: [], mixed: [] };
249
-
250
- let patternMap = new Map(); // structureKey → { structure, count, examples }
251
-
252
- for (let line of lines) {
253
- let email = line.split("@")[0].trim();
254
- if (!email) continue;
255
-
256
- // Разбиваем на токены и разделители
257
- let tokens = [];
258
- let separators = [];
259
- let currentToken = '';
260
- let currentType = '';
261
-
262
- for (let char of email) {
263
- let charType = getCharType(char);
264
- if (currentToken === '' || charType === currentType || (currentType === 'mixed' || charType === 'mixed')) {
265
- currentToken += char;
266
- if (charType !== currentType) currentType = 'mixed';
267
- else if (currentToken.length === 1) currentType = charType;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  } else {
269
- tokens.push({ value: currentToken, type: currentType });
270
- separators.push(char);
271
- allSeparators.add(char);
272
- currentToken = char;
273
- currentType = getCharType(char);
274
  }
 
275
  }
276
- if (currentToken) tokens.push({ value: currentToken, type: currentType });
277
 
278
- // Собираем структуру: [тип_токена, ..., тип_токена] + разделители
279
- let structure = tokens.map(t => t.type);
280
- let structureKey = JSON.stringify(structure) + "|" + separators.join('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
 
282
- if (!patternMap.has(structureKey)) {
283
- patternMap.set(structureKey, {
284
- structure: structure,
285
- separators: [...separators],
286
- count: 0,
287
- examples: [],
288
- tokenLengths: structure.map(() => []),
289
- numberPositions: []
290
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  }
 
 
 
292
 
293
- let pattern = patternMap.get(structureKey);
294
- pattern.count++;
295
- if (pattern.examples.length < 3) pattern.examples.push(email);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
- // Статистика по длинам токенов
298
- tokens.forEach((t, i) => {
299
- pattern.tokenLengths[i].push(t.value.length);
300
- if (t.type === 'number') {
301
- pattern.numberPositions.push(i);
302
- }
303
- if (t.type === 'word') allTokenStats.words.push(t.value);
304
- else if (t.type === 'number') allTokenStats.numbers.push(t.value);
305
- else if (t.type === 'mixed') allTokenStats.mixed.push(t.value);
 
306
  });
307
  }
308
 
309
- dynamicPatterns = Array.from(patternMap.values()).sort((a,b) => b.count - a.count);
310
-
311
- if (!document.getElementById('stealthMode').checked) {
312
- let list = document.getElementById('patternList');
313
- list.innerHTML = '';
314
- dynamicPatterns.forEach(pat => {
315
- let div = document.createElement('div');
316
- div.className = 'pattern-item';
317
- let structureStr = pat.structure.map(t => t[0].toUpperCase()).join('') + ' (' + pat.separators.join('|') + ')';
318
- div.title = pat.examples.join(', ');
319
- div.textContent = `${structureStr}: ${pat.count}`;
320
- list.appendChild(div);
321
  });
322
- document.getElementById('patternInfo').style.display = 'block';
323
  }
324
 
325
- logOperation('dynamic_analyze', { patterns: dynamicPatterns.length, lines: lines.length });
326
- }
 
 
 
327
 
328
- function getCharType(char) {
329
- if (/[0-9]/.test(char)) return 'number';
330
- if (/[\p{L}]/u.test(char)) return 'word';
331
- return 'symbol';
332
- }
 
 
 
 
 
 
 
333
 
334
- function generateFromModel() {
335
- let seed = document.getElementById("seed").value || Date.now().toString();
336
- Math.seedrandom(seed);
 
 
 
 
337
 
338
- let fns = document.getElementById("firstNames").value.split(",").map(x => x.trim().toLowerCase()).filter(x => x);
339
- let lns = document.getElementById("lastNames").value.split(",").map(x => x.trim().toLowerCase()).filter(x => x);
340
- let n = parseInt(document.getElementById("count").value);
341
- let domains = getDomains();
342
 
343
- if (fns.length === 0 || lns.length === 0) {
344
- if (!document.getElementById('stealthMode').checked) alert("Введите имена и фамилии");
345
- return;
346
- }
347
- if (n <= 0 || n > 5000) {
348
- if (!document.getElementById('stealthMode').checked) alert("Количество от 1 до 5000");
349
- return;
350
- }
351
- if (domains.length === 0) {
352
- if (!document.getElementById('stealthMode').checked) alert("Введите хотя бы один домен");
353
- return;
354
- }
355
 
356
- if (dynamicPatterns.length === 0) {
357
- if (!document.getElementById('stealthMode').checked) alert("Сначала загрузите файл для анализа!");
358
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  }
360
 
361
- let out = [];
362
-
363
- for (let i = 0; i < n * 3; i++) {
364
- if (out.length >= n) break;
365
-
366
- // Выбираем случайный паттерн по частоте
367
- let total = dynamicPatterns.reduce((sum, p) => sum + p.count, 0);
368
- let rnd = Math.random() * total;
369
- let acc = 0;
370
- let selectedPattern = dynamicPatterns[0];
371
- for (let p of dynamicPatterns) {
372
- acc += p.count;
373
- if (rnd < acc) { selectedPattern = p; break; }
374
- }
375
-
376
- // Генерируем токены по структуре
377
- let tokens = [];
378
- for (let j = 0; j < selectedPattern.structure.length; j++) {
379
- let type = selectedPattern.structure[j];
380
- let value = '';
381
-
382
- if (type === 'word') {
383
- // Берем случайное имя или фамилию
384
- let pool = Math.random() < 0.5 ? fns : lns;
385
- value = pool[Math.floor(Math.random() * pool.length)];
386
- // Если токен короткий — возможно, это инициал
387
- if (selectedPattern.tokenLengths[j] && selectedPattern.tokenLengths[j].reduce((a,b)=>a+b,0)/selectedPattern.tokenLengths[j].length < 2) {
388
- value = value.charAt(0);
 
 
 
 
 
 
 
 
 
 
 
389
  }
390
- } else if (type === 'number') {
391
- // Генерим число по статистике
392
- if (selectedPattern.numberPositions.includes(j) && selectedPattern.tokenLengths[j]) {
393
- let avgLen = Math.round(selectedPattern.tokenLengths[j].reduce((a,b)=>a+b,0) / selectedPattern.tokenLengths[j].length);
394
- if (avgLen <= 2) value = String(Math.floor(10 + Math.random() * 90));
395
- else if (avgLen === 4) value = String(1980 + Math.floor(Math.random() * 45));
396
- else value = String(Math.floor(Math.pow(10, avgLen-1) + Math.random() * (Math.pow(10, avgLen) - Math.pow(10, avgLen-1))));
397
- } else {
398
- value = String(Math.floor(10 + Math.random() * 900));
399
  }
400
- } else if (type === 'mixed') {
401
- // Смешанный — берем случайный из статистики или генерим
402
- if (allTokenStats.mixed.length > 0 && Math.random() < 0.7) {
403
- value = allTokenStats.mixed[Math.floor(Math.random() * allTokenStats.mixed.length)];
404
- } else {
405
- let w = fns[Math.floor(Math.random() * fns.length)].charAt(0);
406
- let n = Math.floor(10 + Math.random() * 90);
407
- value = w + n;
408
  }
409
  }
410
 
411
- tokens.push(value);
 
 
 
 
 
 
 
 
412
  }
413
 
414
- // Собираем с разделителями
415
- let result = tokens[0];
416
- for (let j = 1; j < tokens.length; j++) {
417
- let sep = j-1 < selectedPattern.separators.length ? selectedPattern.separators[j-1] : '';
418
- result += sep + tokens[j];
419
  }
420
 
421
- let dom = domains[Math.floor(Math.random() * domains.length)];
422
- let email = result.toLowerCase() + "@" + dom;
423
- out.push(email);
 
 
 
 
 
 
424
  }
425
 
426
- // Дедупликация
427
- out = [...new Set(out)];
428
- if (out.length < n) {
429
- while (out.length < n) {
430
- let fn = fns[Math.floor(Math.random() * fns.length)];
431
- let ln = lns[Math.floor(Math.random() * lns.length)];
432
- let num = Math.floor(1000 + Math.random() * 9000);
433
- let base = `${fn}${ln}${num}`;
434
- let dom = domains[Math.floor(Math.random() * domains.length)];
435
- out.push(base.toLowerCase() + "@" + dom);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  }
437
  }
438
- out = out.slice(0, n);
439
-
440
- document.getElementById("output").value = out.join("\n");
441
- logOperation('generate_dynamic', { count: n, seed, sample: out.slice(0,3) });
442
- }
443
-
444
- function getDomains() {
445
- if (currentDomainType === "custom") {
446
- const customDomains = document.getElementById("customDomains").value
447
- .split('\n')
448
- .map(d => d.trim().toLowerCase())
449
- .filter(d => d.length > 0);
450
- return customDomains.length > 0 ? customDomains : defaultDomains;
 
 
451
  }
452
- return defaultDomains;
453
- }
454
-
455
- function generate() {
456
- generateFromModel();
457
- }
458
-
459
- function exportCSV() {
460
- let data = document.getElementById("output").value;
461
- if (!data) return;
462
- let csvContent = "data:text/csv;charset=utf-8," + data.split('\n').map(x => `"${x}"`).join('\n');
463
- let encodedUri = encodeURI(csvContent);
464
- let link = document.createElement("a");
465
- link.setAttribute("href", encodedUri);
466
- link.setAttribute("download", "usernames.csv");
467
- document.body.appendChild(link);
468
- link.click();
469
- document.body.removeChild(link);
470
- logOperation('export', { format: 'csv', count: data.split('\n').length });
471
- }
472
-
473
- function exportJSON() {
474
- let data = document.getElementById("output").value;
475
- if (!data) return;
476
- let lines = data.split('\n').filter(x => x);
477
- let json = JSON.stringify(lines, null, 2);
478
- let blob = new Blob([json], {type: 'application/json'});
479
- let url = URL.createObjectURL(blob);
480
- let a = document.createElement('a');
481
- a.href = url;
482
- a.download = 'usernames.json';
483
- document.body.appendChild(a);
484
- a.click();
485
- document.body.removeChild(a);
486
- URL.revokeObjectURL(url);
487
- logOperation('export', { format: 'json', count: lines.length });
488
- }
489
-
490
- function clearOutput() {
491
- document.getElementById("output").value = '';
492
- logOperation('clear', {});
493
- }
494
-
495
- function logOperation(action, data) {
496
- let logs = JSON.parse(localStorage.getItem('redteam_logs') || '[]');
497
- logs.push({
498
- timestamp: new Date().toISOString(),
499
- action,
500
- data,
501
- userAgent: navigator.userAgent
502
  });
503
- localStorage.setItem('redteam_logs', JSON.stringify(logs.slice(-100)));
504
- }
505
-
506
- document.getElementById("fileInput").addEventListener("change", function(e) {
507
- let file = e.target.files[0];
508
- if (!file) return;
509
- document.getElementById("fileInputLabel").textContent = file.name;
510
- let r = new FileReader();
511
- r.onload = () => {
512
- let lines = r.result.split(/\r?\n/).filter(Boolean);
513
- analyzeDynamicPatterns(lines);
514
- if (!document.getElementById('stealthMode').checked) {
515
- alert(`Динамический анализ завершён! Проанализировано ${lines.length} записей. Найдено паттернов: ${dynamicPatterns.length}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  }
517
- };
518
- r.readAsText(file);
519
- });
520
-
521
- document.querySelectorAll('.domain-option').forEach(option => {
522
- option.addEventListener('click', function() {
523
- document.querySelectorAll('.domain-option').forEach(opt => opt.classList.remove('active'));
524
- this.classList.add('active');
525
- currentDomainType = this.dataset.type;
526
- document.getElementById("customDomainsSection").style.display = currentDomainType === "custom" ? "block" : "none";
527
  });
528
- });
529
 
530
- document.addEventListener('DOMContentLoaded', function() {
531
- // При загрузке скрыть блок паттернов, пока файл не загружен
532
- document.getElementById('patternInfo').style.display = 'none';
533
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
  </script>
535
  </body>
536
  </html>
 
1
+ <!doctype html>
2
  <html lang="ru">
3
  <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Advanced Leak-based Username Generator — willhaben style</title>
7
  <style>
8
+ :root{
9
+ --bg:#0b0f14;--panel:#0f1720;--accent:#39a6ff;--muted:#9fb0c8;--text:#e6f1fb;
10
+ --ok:#5ee07a;--warn:#ffcc33;--err:#ff6b6b;
11
+ }
12
+ *{box-sizing:border-box}
13
+ 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}
14
+ header{padding:20px 22px;border-bottom:1px solid rgba(255,255,255,0.03)}
15
+ h1{margin:0;font-size:18px}
16
+ main{display:grid;grid-template-columns:380px 1fr;gap:16px;padding:18px;align-items:start}
17
+ @media (max-width:980px){main{grid-template-columns:1fr;padding:12px}}
18
+ .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)}
19
+ label{display:block;font-size:12px;color:var(--muted);margin-bottom:6px}
20
+ input[type="file"]{color:var(--text)}
21
+ .row{display:flex;gap:8px;align-items:center}
22
+ input[type="text"], input[type="number"], select, textarea {
23
+ width:100%;padding:10px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:#07121a;color:var(--text);
24
+ }
25
+ textarea{min-height:120px;resize:vertical;font-family:ui-monospace,Menlo,Consolas,monospace}
26
+ button{background:linear-gradient(90deg,var(--accent),#6ad6ff);border:none;color:#04202a;padding:10px 12px;border-radius:8px;font-weight:700;cursor:pointer}
27
+ button.ghost{background:transparent;border:1px solid rgba(255,255,255,0.04);color:var(--text)}
28
+ .muted{color:var(--muted);font-size:13px}
29
+ .small{font-size:12px;color:var(--muted)}
30
+ .stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:8px}
31
+ .stat{background:rgba(255,255,255,0.02);padding:10px;border-radius:8px;text-align:center}
32
+ .stat b{display:block;font-size:18px;color:var(--text)}
33
+ table{width:100%;border-collapse:collapse;font-size:13px;margin-top:8px}
34
+ th,td{padding:6px 8px;border-bottom:1px dashed rgba(255,255,255,0.03);text-align:left}
35
+ th{color:var(--muted);font-weight:600;font-size:12px}
36
+ .controls{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
37
+ .progress{height:10px;background:rgba(255,255,255,0.03);border-radius:8px;overflow:hidden}
38
+ .progress > i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),#6ad6ff);width:0%}
39
+ .footer{padding:16px;color:var(--muted);font-size:13px;text-align:center}
40
+ .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}
41
+ .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}
42
+ .flex{display:flex;gap:8px;align-items:center}
43
+ .right-actions{display:flex;gap:8px;align-items:center;justify-content:flex-end}
44
+ .switch-container {
45
+ display: flex;
46
+ align-items: center;
47
+ gap: 8px;
48
+ margin: 8px 0;
49
+ }
50
+ .switch {
51
+ position: relative;
52
+ display: inline-block;
53
+ width: 40px;
54
+ height: 20px;
55
+ }
56
+ .switch input {
57
+ opacity: 0;
58
+ width: 0;
59
+ height: 0;
60
+ }
61
+ .slider {
62
+ position: absolute;
63
+ cursor: pointer;
64
+ top: 0;
65
+ left: 0;
66
+ right: 0;
67
+ bottom: 0;
68
+ background-color: #333;
69
+ transition: .4s;
70
+ border-radius: 34px;
71
+ }
72
+ .slider:before {
73
+ position: absolute;
74
+ content: "";
75
+ height: 16px;
76
+ width: 16px;
77
+ left: 2px;
78
+ bottom: 2px;
79
+ background-color: white;
80
+ transition: .4s;
81
+ border-radius: 50%;
82
+ }
83
+ input:checked + .slider {
84
+ background-color: var(--accent);
85
+ }
86
+ input:checked + .slider:before {
87
+ transform: translateX(20px);
88
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  </style>
90
  </head>
91
  <body>
92
+ <header>
93
+ <h1>Advanced leak-based username generator robust & streaming</h1>
94
+ <div class="small">Загрузите очищенный список логинов (txt). Анализируем паттерны и генерируем правдоподобные варианты по распределению.</div>
95
+ </header>
96
+
97
+ <main>
98
+ <!-- LEFT: Input / Analysis controls -->
99
+ <div class="card">
100
+ <label>1) Загрузить файл с логинами (txt)</label>
101
+ <input id="fileInput" type="file" accept=".txt" />
102
+ <div class="small" style="margin-top:8px">Поддерживается чтение больших файлов через поток (если браузер поддерживает), иначе — FileReader. Формат: один email на строку.</div>
103
+
104
+ <div style="margin-top:12px">
105
+ <label>2) Настройки анализа</label>
106
+ <div class="small">Автоматическое определение паттернов, доменов, имен, фамилий и суффиксов. Можно задать свои значения или использовать автоматически определенные.</div>
107
+
108
+ <div class="switch-container">
109
+ <label class="small">Автоопределение имен/фамилий</label>
110
+ <label class="switch">
111
+ <input type="checkbox" id="autoDetectNames" checked>
112
+ <span class="slider"></span>
113
+ </label>
114
+ </div>
115
+
116
+ <div style="margin-top:8px" class="row">
117
+ <input id="sampleNames" type="text" placeholder="Примеры имён через запятую (опц.)" />
118
+ <input id="sampleLastnames" type="text" placeholder="Примеры фамилий (опц.)" />
119
+ </div>
120
+
121
+ <div class="switch-container" style="margin-top:8px">
122
+ <label class="small">Автоопределение суффиксов</label>
123
+ <label class="switch">
124
+ <input type="checkbox" id="autoDetectSuffixes" checked>
125
+ <span class="slider"></span>
126
+ </label>
127
+ </div>
128
+
129
+ <div style="margin-top:8px" class="row">
130
+ <label style="margin:0">Доп. суффиксы (через запятую)</label>
131
+ </div>
132
+ <input id="manualSuffixes" type="text" placeholder="123,007,1984,2005" />
133
+
134
+ <div class="switch-container" style="margin-top:8px">
135
+ <label class="small">Использовать ручной домен</label>
136
+ <label class="switch">
137
+ <input type="checkbox" id="useManualDomain">
138
+ <span class="slider"></span>
139
+ </label>
140
+ </div>
141
+
142
+ <input id="manualDomain" type="text" placeholder="Введите домен вручную (например: example.com)" style="margin-top:8px; display: none;" />
143
+ </div>
144
 
145
+ <div style="margin-top:12px">
146
+ <label>3) Анализировать / Сброс</label>
147
+ <div class="controls">
148
+ <button id="analyzeBtn">Проанализировать файл</button>
149
+ <button id="resetBtn" class="ghost">Сбросить состояние</button>
150
+ </div>
151
+ </div>
 
152
 
153
+ <div class="stats" style="margin-top:12px">
154
+ <div class="stat">
155
+ <div class="muted">Строк в файле</div>
156
+ <b id="statLines">0</b>
157
+ </div>
158
+ <div class="stat">
159
+ <div class="muted">Уникальных локал-партов</div>
160
+ <b id="statUnique">0</b>
161
+ </div>
162
+ <div class="stat">
163
+ <div class="muted">Доменов</div>
164
+ <b id="statDomains">0</b>
165
+ </div>
166
+ </div>
167
 
168
+ <div style="margin-top:12px">
169
+ <label>4) Результаты анализа (топ паттернов & домены)</label>
170
+ <div style="max-height:260px;overflow:auto">
171
+ <table id="patternsTable">
172
+ <thead><tr><th>Паттерн</th><th>Частота</th><th>Примеры</th></tr></thead>
173
+ <tbody></tbody>
174
+ </table>
175
+
176
+ <table id="domainsTable" style="margin-top:10px">
177
+ <thead><tr><th>Домен</th><th>Частота</th></tr></thead>
178
+ <tbody></tbody>
179
+ </table>
180
+ </div>
181
  </div>
182
+
183
+ <div style="margin-top:12px">
184
+ <label>5) Настройки генерации</label>
185
+ <div class="small">Укажите количество генерируемых вариантов и опции экспорта.</div>
186
+ <div class="row" style="margin-top:6px">
187
+ <input id="genCount" type="number" min="1" value="500" />
188
+ <select id="exportFormat" title="Формат экспорта">
189
+ <option value="txt">.txt (один в строке)</option>
190
+ <option value="csv">.csv (localpart,domain,pattern)</option>
191
+ <option value="json">.json (массив объектов)</option>
192
+ </select>
193
+ </div>
194
+ <div style="margin-top:8px" class="row">
195
+ <label style="margin:0"><input id="dedupe" type="checkbox" checked /> Удалять дубликаты (может потребовать памяти)</label>
196
+ </div>
197
+ <div style="margin-top:8px" class="row">
198
+ <label style="margin:0"><input id="preferLocalDomains" type="checkbox" checked/> Предпочитать локальные домены (gmx.at, aon.at, liwest.at ...)</label>
199
+ </div>
200
+
201
+ <div class="controls" style="margin-top:10px">
202
+ <button id="generateBtn">Сгенерировать</button>
203
+ <button id="previewBtn" class="ghost">Показать примеры</button>
204
+ </div>
205
+
206
+ <div style="margin-top:10px">
207
+ <div class="small">Прогресс:</div>
208
+ <div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100">
209
+ <i id="progressBar"></i>
210
+ </div>
211
+ </div>
212
+
213
  </div>
214
  </div>
215
+
216
+ <!-- RIGHT: Output / Preview / Export -->
217
+ <div class="card">
218
+ <div style="display:flex;justify-content:space-between;align-items:center">
219
+ <div>
220
+ <label>Выход / предпросмотр</label>
221
+ <div class="small">Вы можете просмотреть выборку, скачать полный результат или скопировать часть.</div>
222
+ </div>
223
+ <div class="right-actions">
224
+ <button id="copyPreview" class="ghost">Копировать видимые</button>
225
+ <button id="downloadBtn" class="ghost" disabled>Скачать результат</button>
226
+ </div>
227
+ </div>
228
+
229
+ <div style="margin-top:8px" class="codebox" id="previewBox" aria-live="polite"></div>
230
+
231
+ <div style="margin-top:12px" class="flex">
232
+ <div style="flex:1">
233
+ <label class="small">Фильтр предпросмотра</label>
234
+ <input id="previewFilter" type="text" placeholder="например: .gmx.at или 2005 или alex" />
235
+ </div>
236
+ <div style="width:160px">
237
+ <label class="small">Показать N примеров</label>
238
+ <input id="previewLimit" type="number" min="1" value="50" />
239
+ </div>
240
+ </div>
241
+
242
+ <div style="margin-top:10px" class="small">Экспорт может быть большим — для больших объёмов генерируем файл как Blob и даём ссылку для скачивания.</div>
243
+
244
+ <div style="margin-top:12px">
245
+ <label class="small">Лог действий</label>
246
+ <div id="logBox" class="codebox" style="max-height:160px"></div>
247
+ </div>
248
  </div>
249
+ </main>
250
+
251
+ <div class="footer card">
252
+ <div class="small">Инструмент анализирует паттерны локальной части email (localpart) — firstname.lastname, initials, nick+digits, pure nicks и т.д. — и строит распределение для генерации. Процесс оптимизирован для больших файлов (поточное чтение, батчи и асинхронная генерация).</div>
253
  </div>
254
 
255
  <script>
256
+ (() => {
257
+ const el = id => document.getElementById(id);
258
+ const log = (t) => {
259
+ const b = el('logBox');
260
+ const time = new Date().toISOString().replace('T',' ').split('.')[0];
261
+ b.textContent = `${time} — ${t}\n` + b.textContent;
262
+ };
263
+
264
+ function safeLower(s){ return (s||'').trim().toLowerCase(); }
265
+ function uniqArray(arr){ return [...new Set(arr)]; }
266
+ function sum(obj){
267
+ return Object.values(obj).reduce((a,b)=>a+(b||0),0);
268
+ }
269
+ function pickWeighted(map){
270
+ const entries = Object.entries(map);
271
+ const total = entries.reduce((a,[,w])=>a+w,0);
272
+ if (total === 0) return entries.length ? entries[0][0] : null;
273
+ let r = Math.random()*total, acc=0;
274
+ for(const [k,w] of entries){ acc+=w; if(r < acc) return k; }
275
+ return entries[0][0];
276
+ }
277
+
278
+ const PATTERNS = {
279
+ 'fn.dot.ln': /^[a-zäöüß]+[.][a-zäöüß]+[0-9]*$/i,
280
+ 'fi.dot.ln': /^[a-zäöüß][.][a-zäöüß]+[0-9]*$/i,
281
+ 'fnln': /^[a-zäöüß]+[a-zäöüß]+[0-9]*$/i,
282
+ 'nick_digits': /^[a-z0-9._-]*[0-9]{2,6}$/i,
283
+ 'nick': /^[a-z][a-z0-9._-]{1,20}$/i,
284
+ 'initials': /^[a-z]{2,4}$/i,
285
+ 'contains_special': /[^a-z0-9._-]/i
286
+ };
287
+
288
+ let state = {
289
+ totalLines: 0,
290
+ uniqueLocalparts: new Set(),
291
+ domainCounts: Object.create(null),
292
+ patternCounts: Object.create(null),
293
+ patternExamples: Object.create(null),
294
+ sampleLocalparts: [],
295
+ domainsList: [],
296
+ manualSuffixes: [],
297
+ detectedFirstNames: new Set(),
298
+ detectedLastNames: new Set(),
299
+ detectedSuffixes: new Set(),
300
+ analyzed: false
301
+ };
302
+
303
+ Object.keys(PATTERNS).forEach(k => { state.patternCounts[k]=0; state.patternExamples[k]=new Set(); });
304
+
305
+ el('useManualDomain').addEventListener('change', function() {
306
+ el('manualDomain').style.display = this.checked ? 'block' : 'none';
307
+ });
308
+
309
+ async function processFile(file) {
310
+ state.totalLines = 0;
311
+ state.uniqueLocalparts = new Set();
312
+ state.domainCounts = {};
313
+ state.detectedFirstNames = new Set();
314
+ state.detectedLastNames = new Set();
315
+ state.detectedSuffixes = new Set();
316
+ Object.keys(state.patternCounts).forEach(k => state.patternCounts[k]=0);
317
+ Object.keys(state.patternExamples).forEach(k => state.patternExamples[k].clear());
318
+ state.sampleLocalparts = [];
319
+
320
+ el('statLines').textContent = '0';
321
+ el('statUnique').textContent = '0';
322
+ el('statDomains').textContent = '0';
323
+ el('patternsTable').querySelector('tbody').innerHTML = '';
324
+ el('domainsTable').querySelector('tbody').innerHTML = '';
325
+ el('previewBox').textContent = '';
326
+ el('logBox').textContent = '';
327
+
328
+ log(`Начало анализа файла: ${file.name}`);
329
+
330
+ if (file.stream && typeof file.stream === 'function') {
331
+ log('Использую File.stream() для по-частного чтения (stream).');
332
+ await streamFileProcessing(file);
333
+ } else {
334
+ log('File.stream() не поддерживается — использую FileReader (fallback).');
335
+ await fileReaderProcessing(file);
336
+ }
337
+
338
+ state.domainsList = Object.keys(state.domainCounts).sort((a,b)=>state.domainCounts[b]-state.domainCounts[a]);
339
+ el('statLines').textContent = String(state.totalLines);
340
+ el('statUnique').textContent = String(state.uniqueLocalparts.size);
341
+ el('statDomains').textContent = String(Object.keys(state.domainCounts).length);
342
+
343
+ if (el('autoDetectNames').checked) {
344
+ const firstNames = Array.from(state.detectedFirstNames).slice(0, 20);
345
+ const lastNames = Array.from(state.detectedLastNames).slice(0, 20);
346
+
347
+ if (firstNames.length > 0 && !el('sampleNames').value) {
348
+ el('sampleNames').value = firstNames.join(',');
349
+ log(`Автоопределено ${firstNames.length} имен`);
350
+ }
351
+
352
+ if (lastNames.length > 0 && !el('sampleLastnames').value) {
353
+ el('sampleLastnames').value = lastNames.join(',');
354
+ log(`Автоопределено ${lastNames.length} фамилий`);
355
+ }
356
+ }
357
+
358
+ if (el('autoDetectSuffixes').checked) {
359
+ const suffixes = Array.from(state.detectedSuffixes).slice(0, 15);
360
+
361
+ if (suffixes.length > 0 && !el('manualSuffixes').value) {
362
+ el('manualSuffixes').value = suffixes.join(',');
363
+ log(`Автоопределено ${suffixes.length} суффиксов`);
364
+ }
365
+ }
366
+
367
+ renderPatternTable();
368
+ renderDomainTable();
369
+ state.analyzed = true;
370
+ log('Анализ завершён.');
371
+ }
372
+
373
+ async function streamFileProcessing(file){
374
+ const decoder = new TextDecoder();
375
+ const reader = file.stream().getReader();
376
+ let { value: chunk, done: readerDone } = await reader.read();
377
+ let buffer = '';
378
+ while (!readerDone) {
379
+ buffer += decoder.decode(chunk, { stream: true });
380
+ const lines = buffer.split(/\r\n|\n/);
381
+ buffer = lines.pop();
382
+ await processLineBatch(lines);
383
+ const res = await reader.read();
384
+ chunk = res.value; readerDone = res.done;
385
+ }
386
+ if (buffer) await processLineBatch([buffer]);
387
+ await reader.releaseLock?.();
388
+ }
389
+
390
+ async function fileReaderProcessing(file){
391
+ const CHUNK = 2 * 1024 * 1024;
392
+ let offset = 0;
393
+ while (offset < file.size) {
394
+ const slice = file.slice(offset, offset + CHUNK);
395
+ const text = await slice.text();
396
+ const lines = text.split(/\r\n|\n/);
397
+ if (offset + CHUNK < file.size) {
398
+ const last = lines.pop();
399
+ await processLineBatch(lines);
400
+ offset += CHUNK - (last ? last.length : 0);
401
  } else {
402
+ await processLineBatch(lines);
403
+ offset += CHUNK;
 
 
 
404
  }
405
+ await new Promise(r => setTimeout(r, 0));
406
  }
407
+ }
408
 
409
+ async function processLineBatch(lines) {
410
+ for (const rawLine of lines) {
411
+ const line = (rawLine||'').trim();
412
+ if (!line) continue;
413
+ state.totalLines++;
414
+ const atIdx = line.indexOf('@');
415
+ const local = safeLower(atIdx === -1 ? line : line.slice(0, atIdx));
416
+ const domain = atIdx === -1 ? '' : safeLower(line.slice(atIdx+1));
417
+ if (!local) continue;
418
+
419
+ state.uniqueLocalparts.add(local);
420
+ if (domain) state.domainCounts[domain] = (state.domainCounts[domain]||0) + 1;
421
+
422
+ classifyLocal(local);
423
+ extractNamesFromLocal(local);
424
+ extractSuffixesFromLocal(local);
425
+
426
+ if (state.sampleLocalparts.length < 200) state.sampleLocalparts.push({local, domain});
427
+ }
428
+ if (state.totalLines % 500 === 0) {
429
+ el('statLines').textContent = String(state.totalLines);
430
+ el('statUnique').textContent = String(state.uniqueLocalparts.size);
431
+ el('statDomains').textContent = String(Object.keys(state.domainCounts).length);
432
+ await new Promise(r=>setTimeout(r,0));
433
+ }
434
+ }
435
 
436
+ function classifyLocal(local){
437
+ if (PATTERNS['fn.dot.ln'].test(local)) {
438
+ state.patternCounts['fn.dot.ln']++;
439
+ addExample('fn.dot.ln', local);
440
+ return;
441
+ }
442
+ if (PATTERNS['fi.dot.ln'].test(local)) {
443
+ state.patternCounts['fi.dot.ln']++;
444
+ addExample('fi.dot.ln', local);
445
+ return;
446
+ }
447
+ if (PATTERNS['nick_digits'].test(local)) {
448
+ state.patternCounts['nick_digits']++;
449
+ addExample('nick_digits', local);
450
+ return;
451
+ }
452
+ if (PATTERNS['initials'].test(local)) {
453
+ state.patternCounts['initials']++;
454
+ addExample('initials', local);
455
+ return;
456
+ }
457
+ if (PATTERNS['nick'].test(local)) {
458
+ state.patternCounts['nick']++;
459
+ addExample('nick', local);
460
+ return;
461
+ }
462
+ if (PATTERNS['contains_special'].test(local)) {
463
+ state.patternCounts['contains_special']++;
464
+ addExample('contains_special', local);
465
+ return;
466
  }
467
+ state.patternCounts['fnln']++;
468
+ addExample('fnln', local);
469
+ }
470
 
471
+ function addExample(pat, local) {
472
+ const s = state.patternExamples[pat];
473
+ if (s && s.size < 8) s.add(local);
474
+ }
475
+
476
+ function extractNamesFromLocal(local) {
477
+ const fnLnMatch = local.match(/^([a-zäöüß]{2,20})[._]([a-zäöüß]{2,20})/i);
478
+ if (fnLnMatch) {
479
+ state.detectedFirstNames.add(fnLnMatch[1]);
480
+ state.detectedLastNames.add(fnLnMatch[2]);
481
+ }
482
+
483
+ const fnDigitsMatch = local.match(/^([a-zäöüß]{2,20})[0-9]{2,6}$/i);
484
+ if (fnDigitsMatch) {
485
+ state.detectedFirstNames.add(fnDigitsMatch[1]);
486
+ }
487
+
488
+ const lnDigitsMatch = local.match(/^[a-zäöüß]{1}[._]([a-zäöüß]{2,20})[0-9]{2,6}$/i);
489
+ if (lnDigitsMatch) {
490
+ state.detectedLastNames.add(lnDigitsMatch[1]);
491
+ }
492
+
493
+ const initialsLnMatch = local.match(/^[a-z]{1,2}[._]([a-zäöüß]{2,20})/i);
494
+ if (initialsLnMatch) {
495
+ state.detectedLastNames.add(initialsLnMatch[1]);
496
+ }
497
+ }
498
+
499
+ function extractSuffixesFromLocal(local) {
500
+ const digitsMatch = local.match(/[a-zäöüß][._-]?([0-9]{2,6})$/i);
501
+ if (digitsMatch) {
502
+ state.detectedSuffixes.add(digitsMatch[1]);
503
+ }
504
+
505
+ const yearMatch = local.match(/(19[0-9]{2}|20[0-9]{2})$/);
506
+ if (yearMatch) {
507
+ state.detectedSuffixes.add(yearMatch[1]);
508
+ }
509
+
510
+ const shortNumMatch = local.match(/([0-9]{2,4})$/);
511
+ if (shortNumMatch && !yearMatch) {
512
+ state.detectedSuffixes.add(shortNumMatch[1]);
513
+ }
514
+ }
515
 
516
+ function renderPatternTable(){
517
+ const tbody = el('patternsTable').querySelector('tbody');
518
+ tbody.innerHTML = '';
519
+ const total = sum(state.patternCounts) || 1;
520
+ Object.entries(state.patternCounts).sort((a,b)=>b[1]-a[1]).forEach(([p,c])=>{
521
+ const tr = document.createElement('tr');
522
+ const examples = [...(state.patternExamples[p]||[])].slice(0,5).join(', ');
523
+ const pct = ((c/total)*100).toFixed(1) + '%';
524
+ tr.innerHTML = `<td>${p}</td><td>${c} (${pct})</td><td class="small">${examples}</td>`;
525
+ tbody.appendChild(tr);
526
  });
527
  }
528
 
529
+ function renderDomainTable(){
530
+ const tbody = el('domainsTable').querySelector('tbody');
531
+ tbody.innerHTML = '';
532
+ Object.entries(state.domainCounts).sort((a,b)=>b[1]-a[1]).slice(0,100).forEach(([d,c])=>{
533
+ const tr = document.createElement('tr');
534
+ tr.innerHTML = `<td>@${d}</td><td>${c}</td>`;
535
+ tbody.appendChild(tr);
 
 
 
 
 
536
  });
 
537
  }
538
 
539
+ function buildGenerators() {
540
+ const patternWeights = {};
541
+ Object.keys(state.patternCounts).forEach(k => {
542
+ patternWeights[k] = state.patternCounts[k] || 1;
543
+ });
544
 
545
+ let domainWeights = {};
546
+
547
+ if (el('useManualDomain').checked && el('manualDomain').value.trim()) {
548
+ const manualDomain = safeLower(el('manualDomain').value);
549
+ domainWeights = {};
550
+ domainWeights[manualDomain] = 1000;
551
+ log(`Используется ручной домен: ${manualDomain}`);
552
+ } else {
553
+ Object.entries(state.domainCounts).forEach(([d,w]) => domainWeights[d] = w);
554
+ if (Object.keys(domainWeights).length === 0) {
555
+ ['gmail.com','gmx.at','aon.at','yahoo.com','outlook.com','web.de','chello.at','liwest.at','hotmail.com'].forEach(d=>domainWeights[d] = 10);
556
+ }
557
 
558
+ if (el('preferLocalDomains').checked) {
559
+ const localPrefer = ['gmx.at','aon.at','chello.at','liwest.at','inode.at','student.uibk.ac.at','proton.me','protonmail.com'];
560
+ localPrefer.forEach(d=>{
561
+ if (domainWeights[d]) domainWeights[d] *= 3;
562
+ });
563
+ }
564
+ }
565
 
566
+ const manual = el('manualSuffixes').value.split(',').map(s=>s.trim()).filter(Boolean);
567
+ const manualSuffixes = uniqArray(manual);
 
 
568
 
569
+ const providedFirsts = el('sampleNames').value.split(',').map(x=>safeLower(x)).filter(Boolean);
570
+ const providedLasts = el('sampleLastnames').value.split(',').map(x=>safeLower(x)).filter(Boolean);
 
 
 
 
 
 
 
 
 
 
571
 
572
+ const guessedFirsts = new Set();
573
+ const guessedLasts = new Set();
574
+ state.sampleLocalparts.forEach(({local})=>{
575
+ const m = local.match(/^([a-zäöüß]{2,20})[._]([a-zäöüß]{2,20})/i);
576
+ if (m) { guessedFirsts.add(m[1]); guessedLasts.add(m[2]); }
577
+ else {
578
+ const m2 = local.match(/^([a-zäöüß]{2,20})[0-9]{2,6}$/i);
579
+ if (m2) guessedFirsts.add(m2[1]);
580
+ }
581
+ });
582
+
583
+ const firstPool = uniqArray([...providedFirsts, ...Array.from(guessedFirsts)]).filter(Boolean);
584
+ const lastPool = uniqArray([...providedLasts, ...Array.from(guessedLasts)]).filter(Boolean);
585
+ if (firstPool.length === 0) firstPool.push('alex','maria','stefan','anna','thomas','martin');
586
+ if (lastPool.length === 0) lastPool.push('bauer','schmidt','mueller','hofer','gruber','wagner');
587
+
588
+ const specialLocals = Array.from(state.uniqueLocalparts).filter(x => PATTERNS['contains_special'].test(x));
589
+
590
+ const gens = {
591
+ 'fn.dot.ln': () => {
592
+ const fn = firstPool[Math.floor(Math.random()*firstPool.length)];
593
+ const ln = lastPool[Math.floor(Math.random()*lastPool.length)];
594
+ return `${fn}.${ln}`;
595
+ },
596
+ 'fi.dot.ln': () => {
597
+ const fn = firstPool[Math.floor(Math.random()*firstPool.length)];
598
+ const ln = lastPool[Math.floor(Math.random()*lastPool.length)];
599
+ return `${fn[0]}.${ln}`;
600
+ },
601
+ 'fnln': () => {
602
+ const fn = firstPool[Math.floor(Math.random()*firstPool.length)];
603
+ const ln = lastPool[Math.floor(Math.random()*lastPool.length)];
604
+ return `${fn}${ln}`;
605
+ },
606
+ 'nick_digits': () => {
607
+ const pick = Math.random()<0.5 ? firstPool[Math.floor(Math.random()*firstPool.length)] : (sampleNickFromDataset() || firstPool[Math.floor(Math.random()*firstPool.length)]);
608
+ const digits = getRandomSuffix();
609
+ return `${pick}${digits}`;
610
+ },
611
+ 'nick': () => {
612
+ return sampleNickFromDataset() || firstPool[Math.floor(Math.random()*firstPool.length)];
613
+ },
614
+ 'initials': () => {
615
+ const fn = firstPool[Math.floor(Math.random()*firstPool.length)];
616
+ const ln = lastPool[Math.floor(Math.random()*lastPool.length)];
617
+ return `${fn[0]}${ln[0]}`;
618
+ },
619
+ 'contains_special': () => {
620
+ if (specialLocals.length > 0) {
621
+ return specialLocals[Math.floor(Math.random()*specialLocals.length)];
622
+ }
623
+ return sampleNickFromDataset() || 'user' + Math.floor(100+Math.random()*899);
624
+ }
625
+ };
626
+
627
+ function sampleNickFromDataset(){
628
+ const arr = Array.from(state.uniqueLocalparts).filter(x => /^[a-z][a-z0-9._-]{1,20}$/i.test(x));
629
+ if (arr.length === 0) return null;
630
+ return arr[Math.floor(Math.random()*arr.length)].replace(/[._-]+$/,'').replace(/\s+/g,'');
631
+ }
632
+
633
+ function getRandomSuffix(){
634
+ const ms = manualSuffixes.filter(Boolean);
635
+ const picks = [];
636
+ if (ms.length) picks.push(...ms);
637
+ const common = ['007','123','69','84','97','2000','2001','2010','2012'];
638
+ picks.push(...common);
639
+ picks.push(String(100 + Math.floor(Math.random()*899)).slice(1));
640
+ return picks[Math.floor(Math.random()*picks.length)];
641
+ }
642
+
643
+ return {patternWeights, domainWeights, gens, firstPool, lastPool};
644
  }
645
 
646
+ async function generateToBlob(count, opts) {
647
+ const {patternWeights, domainWeights, gens} = buildGenerators();
648
+ const encoder = new TextEncoder();
649
+ const chunks = [];
650
+ const dedupeSet = opts.dedupe ? new Set() : null;
651
+ let produced = 0;
652
+ const totalTarget = count;
653
+ const batchSize = 10000;
654
+ let buffer = [];
655
+ let bufferBytes = 0;
656
+
657
+ function pickPattern() {
658
+ return pickWeighted(patternWeights);
659
+ }
660
+
661
+ function pickDomain(){
662
+ return pickWeighted(domainWeights);
663
+ }
664
+
665
+ function genOne(){
666
+ const pat = pickPattern();
667
+ const local = (gens[pat] ? gens[pat]() : gens['fnln']()).replace(/\s+/g,'').toLowerCase();
668
+ const domain = pickDomain();
669
+ return {local, domain, pattern: pat};
670
+ }
671
+
672
+ const fmt = opts.exportFormat || 'txt';
673
+ if (fmt === 'json') {
674
+ chunks.push(encoder.encode('['));
675
+ }
676
+
677
+ while (produced < totalTarget) {
678
+ const toMake = Math.min(batchSize, totalTarget - produced);
679
+ for (let i=0; i<toMake; i++) {
680
+ const item = genOne();
681
+ const lineTxt = `${item.local}@${item.domain}`;
682
+ if (dedupeSet) {
683
+ if (dedupeSet.has(lineTxt)) continue;
684
+ dedupeSet.add(lineTxt);
685
  }
686
+ let line;
687
+ if (fmt === 'txt') {
688
+ line = lineTxt + '\n';
689
+ } else if (fmt === 'csv') {
690
+ line = `"${item.local}","${item.domain}","${item.pattern}"\n`;
691
+ } else if (fmt === 'json') {
692
+ line = (produced > 0 ? ',' : '') + JSON.stringify({local:item.local,domain:item.domain,pattern:item.pattern}) + '\n';
 
 
693
  }
694
+ buffer.push(line);
695
+ bufferBytes += line.length;
696
+ produced++;
697
+
698
+ if (buffer.length >= 5000 || bufferBytes > 1024 * 1024) {
699
+ flushBuffer();
 
 
700
  }
701
  }
702
 
703
+ if (opts.progressCallback && produced % 5000 === 0) {
704
+ const currentCount = dedupeSet ? dedupeSet.size : produced;
705
+ opts.progressCallback(Math.min(100, Math.round((currentCount / totalTarget) * 100)), currentCount);
706
+ await new Promise(r => requestAnimationFrame(r));
707
+ }
708
+ }
709
+
710
+ if (buffer.length > 0) {
711
+ flushBuffer();
712
  }
713
 
714
+ if (fmt === 'json') {
715
+ chunks.push(encoder.encode(']'));
 
 
 
716
  }
717
 
718
+ return new Blob(chunks, {type: fmt === 'json' ? 'application/json;charset=utf-8' : 'text/plain;charset=utf-8'});
719
+
720
+ function flushBuffer() {
721
+ if (buffer.length === 0) return;
722
+ const text = buffer.join('');
723
+ chunks.push(encoder.encode(text));
724
+ buffer = [];
725
+ bufferBytes = 0;
726
+ }
727
  }
728
 
729
+ let lastGeneratedBlobURL = null;
730
+ async function handleGenerate() {
731
+ if (!state.analyzed) {
732
+ alert('Сначала выполните анализ файла (Analyze file).');
733
+ return;
734
+ }
735
+ const count = Math.max(1, parseInt(el('genCount').value || '100',10));
736
+ const format = el('exportFormat').value;
737
+ const dedupe = el('dedupe').checked;
738
+ el('generateBtn').disabled = true;
739
+ el('generateBtn').textContent = 'Генерация...';
740
+ log(`Старт генерации: ${count} элементов, формат=${format}, dedupe=${dedupe}`);
741
+
742
+ try {
743
+ const blob = await generateToBlob(count, {
744
+ dedupe,
745
+ exportFormat: format,
746
+ progressCallback: (pct, current) => {
747
+ el('progressBar').style.width = pct + '%';
748
+ el('progressBar').setAttribute('aria-valuenow', pct);
749
+ el('previewBox').textContent = `Генерация: ${current} / ${count} (${pct}%)`;
750
+ }
751
+ });
752
+
753
+ if (lastGeneratedBlobURL) URL.revokeObjectURL(lastGeneratedBlobURL);
754
+ lastGeneratedBlobURL = URL.createObjectURL(blob);
755
+ el('downloadBtn').disabled = false;
756
+ el('downloadBtn').textContent = `Скачать (${(blob.size/1024/1024).toFixed(2)} MB)`;
757
+ el('downloadBtn').onclick = () => {
758
+ const a = document.createElement('a');
759
+ let ext = '.txt';
760
+ if (format==='csv') ext = '.csv';
761
+ if (format==='json') ext = '.json';
762
+ a.href = lastGeneratedBlobURL;
763
+ a.download = `generated_usernames${ext}`;
764
+ document.body.appendChild(a);
765
+ a.click();
766
+ a.remove();
767
+ };
768
+
769
+ const previewLimit = Math.max(10, Math.min(1000, parseInt(el('previewLimit').value || '50',10)));
770
+ const previewTxt = await readBlobHead(blob, previewLimit);
771
+ el('previewBox').textContent = previewTxt;
772
+
773
+ // Apply filter after generation
774
+ const filter = (el('previewFilter').value || '').toLowerCase();
775
+ if (filter) {
776
+ const lines = previewTxt.split(/\r\n|\n/).filter(Boolean);
777
+ const shown = lines.filter(l => l.includes(filter)).slice(0, previewLimit);
778
+ el('previewBox').textContent = shown.join('\n');
779
+ }
780
+
781
+ log('Генерация завершена успешно.');
782
+ } catch (e) {
783
+ log('Ошибка при генерации: ' + (e && e.message ? e.message : String(e)));
784
+ alert('Ошибка генерации: ' + (e && e.message ? e.message : String(e)));
785
+ } finally {
786
+ el('generateBtn').disabled = false;
787
+ el('generateBtn').textContent = 'Сгенерировать';
788
+ el('progressBar').style.width = '0%';
789
+ el('progressBar').setAttribute('aria-valuenow', 0);
790
  }
791
  }
792
+
793
+ async function readBlobHead(blob, limitLines){
794
+ const CHUNK = 64 * 1024;
795
+ let offset = 0;
796
+ let decoder = new TextDecoder();
797
+ let accumulated = '';
798
+ while (offset < blob.size && accumulated.split(/\r\n|\n/).length - 1 < limitLines) {
799
+ const slice = blob.slice(offset, offset + CHUNK);
800
+ const text = await slice.text();
801
+ accumulated += text;
802
+ offset += CHUNK;
803
+ await new Promise(r=>setTimeout(r,0));
804
+ }
805
+ const lines = accumulated.split(/\r\n|\n/).filter(Boolean).slice(0, limitLines);
806
+ return lines.join('\n');
807
  }
808
+
809
+ el('analyzeBtn').addEventListener('click', async () => {
810
+ const file = el('fileInput').files && el('fileInput').files[0];
811
+ if (!file) { alert('Выберите файл .txt со списком логинов.'); return; }
812
+ try {
813
+ el('analyzeBtn').disabled = true;
814
+ el('analyzeBtn').textContent = 'Анализ...';
815
+ await processFile(file);
816
+ } catch (e) {
817
+ log('Ошибка при анализе: ' + (e && e.message ? e.message : String(e)));
818
+ alert('Ошибка при анализе файла: ' + (e && e.message ? e.message : String(e)));
819
+ } finally {
820
+ el('analyzeBtn').disabled = false;
821
+ el('analyzeBtn').textContent = 'Проанализировать файл';
822
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
823
  });
824
+
825
+ el('resetBtn').addEventListener('click', () => {
826
+ if (!confirm('Сбросить текущее состояние анал��за?')) return;
827
+ state = {
828
+ totalLines: 0,
829
+ uniqueLocalparts: new Set(),
830
+ domainCounts: Object.create(null),
831
+ patternCounts: Object.create(null),
832
+ patternExamples: Object.create(null),
833
+ sampleLocalparts: [],
834
+ domainsList: [],
835
+ manualSuffixes: [],
836
+ detectedFirstNames: new Set(),
837
+ detectedLastNames: new Set(),
838
+ detectedSuffixes: new Set(),
839
+ analyzed: false
840
+ };
841
+ Object.keys(PATTERNS).forEach(k => { state.patternCounts[k]=0; state.patternExamples[k]=new Set(); });
842
+ el('statLines').textContent = '0';
843
+ el('statUnique').textContent = '0';
844
+ el('statDomains').textContent = '0';
845
+ el('patternsTable').querySelector('tbody').innerHTML = '';
846
+ el('domainsTable').querySelector('tbody').innerHTML = '';
847
+ el('previewBox').textContent = '';
848
+ el('logBox').textContent = '';
849
+ el('downloadBtn').disabled = true;
850
+ if (lastGeneratedBlobURL) { URL.revokeObjectURL(lastGeneratedBlobURL); lastGeneratedBlobURL = null; }
851
+ log('Состояние сброшено пользователем.');
852
+ });
853
+
854
+ el('generateBtn').addEventListener('click', handleGenerate);
855
+
856
+ el('previewBtn').addEventListener('click', () => {
857
+ if (!state.analyzed) { alert('Сначала выполните анализ файла.'); return; }
858
+ const limit = Math.max(10, Math.min(200, parseInt(el('previewLimit').value || '50',10)));
859
+ const filter = (el('previewFilter').value || '').toLowerCase();
860
+ const samples = state.sampleLocalparts.map(x => `${x.local}@${x.domain||'[no-domain]'}`);
861
+ const shown = (filter ? samples.filter(s => s.includes(filter)) : samples).slice(0, limit);
862
+ el('previewBox').textContent = shown.join('\n');
863
+ });
864
+
865
+ el('copyPreview').addEventListener('click', async () => {
866
+ const txt = el('previewBox').textContent.trim();
867
+ if (!txt) return alert('Нет текста для копирования.');
868
+ try {
869
+ await navigator.clipboard.writeText(txt);
870
+ log('Предпросмотр скопирован в буфер обмена.');
871
+ } catch (e) {
872
+ alert('Не удалось скопировать: ' + (e.message||e));
873
  }
 
 
 
 
 
 
 
 
 
 
874
  });
 
875
 
876
+ el('previewFilter').addEventListener('input', () => {
877
+ const filter = (el('previewFilter').value || '').toLowerCase();
878
+ if (!el('previewBox').textContent) return;
879
+ const lines = el('previewBox').textContent.split(/\r\n|\n/).filter(Boolean);
880
+ const shown = filter ? lines.filter(l => l.includes(filter)) : lines;
881
+ el('previewBox').textContent = shown.slice(0, Math.max(50, parseInt(el('previewLimit').value||'50'))).join('\n');
882
+ });
883
+
884
+ el('fileInput').addEventListener('change', () => {
885
+ el('statLines').textContent = '0';
886
+ el('statUnique').textContent = '0';
887
+ el('statDomains').textContent = '0';
888
+ el('patternsTable').querySelector('tbody').innerHTML = '';
889
+ el('domainsTable').querySelector('tbody').innerHTML = '';
890
+ el('previewBox').textContent = '';
891
+ el('logBox').textContent = '';
892
+ state.analyzed = false;
893
+ if (lastGeneratedBlobURL) { URL.revokeObjectURL(lastGeneratedBlobURL); lastGeneratedBlobURL = null; el('downloadBtn').disabled = true; }
894
+ log('Новый файл выбран, предыдущий анализ отменён.');
895
+ });
896
+
897
+ log('Готов к работе. Загрузите файл и нажмите "Проанализировать файл".');
898
+
899
+ })();
900
  </script>
901
  </body>
902
  </html>