Add email address generator: 3 styles, 14 domains, inline menu, /email command, tests
This commit is contained in:
1 parent
20c3fb1c06
commit
3b92e317d0
4 files changed
+158
-2
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import 'dotenv/config';
|
||||
import fs from 'node:fs';
|
||||
import { Telegraf, Markup } from 'telegraf';
|
||||
import { generatePassword, strength } from './lib/generator.js';
|
||||
import { generatePassword, strength, generateEmail, EMAIL_DOMAINS } from './lib/generator.js';
|
||||
|
||||
const boot = (m) =>
|
||||
fs.appendFileSync(
|
||||
@@ -28,6 +28,9 @@ const defaults = () => ({
|
||||
symbols: true,
|
||||
excludeAmbiguous: false,
|
||||
pronounceable: false,
|
||||
emailStyle: 0,
|
||||
emailDomain: 0,
|
||||
emailNumber: true,
|
||||
});
|
||||
const users = new Map();
|
||||
const history = new Map(); // userId -> [passwords]
|
||||
@@ -49,6 +52,7 @@ const mainMenu = () =>
|
||||
Markup.inlineKeyboard([
|
||||
[Markup.button.callback('🔑 Сгенерировать пароль', 'gen')],
|
||||
[Markup.button.callback('🎰 Пачка из 5', 'gen5')],
|
||||
[Markup.button.callback('📧 Почтовый адрес', 'email')],
|
||||
[Markup.button.callback('⚙️ Настройки', 'settings')],
|
||||
[Markup.button.callback('📋 История', 'history')],
|
||||
[Markup.button.callback('ℹ️ О боте', 'about')],
|
||||
@@ -107,6 +111,39 @@ const passwordText = (pwd, s) => {
|
||||
].join('\n');
|
||||
};
|
||||
|
||||
const EMAIL_STYLES = ['random (psxaiq)', 'pronounceable (sevemafi)', 'wordnum (nova.turbo86)'];
|
||||
const EMAIL_STYLE_KEYS = ['random', 'pronounceable', 'wordnum'];
|
||||
|
||||
const emailKb = (s) =>
|
||||
Markup.inlineKeyboard([
|
||||
[Markup.button.callback('🔁 Ещё раз', 'email'), Markup.button.callback('🎰 Пачка из 5', 'email5')],
|
||||
[
|
||||
Markup.button.callback('◀️', 'edom:-1'),
|
||||
Markup.button.callback('🌐 ' + EMAIL_DOMAINS[s.emailDomain], 'noop'),
|
||||
Markup.button.callback('▶️', 'edom:+1'),
|
||||
],
|
||||
[Markup.button.callback('🎨 Стиль: ' + EMAIL_STYLES[s.emailStyle], 'estyle')],
|
||||
[
|
||||
Markup.button.callback((s.emailNumber ? '✅' : '❎') + ' Цифра в адресе', 'enum'),
|
||||
Markup.button.callback('⬅️ Меню', 'main'),
|
||||
],
|
||||
]);
|
||||
|
||||
const emailText = (mail, s) =>
|
||||
'📧 Почтовый адрес:\n\n' +
|
||||
BT2(mail) +
|
||||
'\n\n🎨 Стиль: ' + EMAIL_STYLES[s.emailStyle] + '\n' +
|
||||
'🌐 Домен: ' + EMAIL_DOMAINS[s.emailDomain] + '\n\n' +
|
||||
'🛡 crypto.randomInt — нажми на адрес, чтобы скопировать.';
|
||||
const BT2 = (x) => "`" + x + "`" + '\n\n';
|
||||
|
||||
const makeEmail = (s) =>
|
||||
generateEmail({
|
||||
style: EMAIL_STYLE_KEYS[s.emailStyle] || 'random',
|
||||
domain: EMAIL_DOMAINS[s.emailDomain],
|
||||
withNumber: s.emailNumber,
|
||||
length: Math.max(6, Math.min(s.length, 20)),
|
||||
});
|
||||
/* ---------- Shared actions ---------- */
|
||||
const sendPassword = async (ctx, s) => {
|
||||
const pwd = generatePassword(s);
|
||||
@@ -211,6 +248,63 @@ bot.action('gen5', async (ctx) => {
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
bot.action('email', (ctx) => {
|
||||
const s = state(ctx.from.id);
|
||||
const mail = makeEmail(s);
|
||||
addHistory(ctx.from.id, '📧 ' + mail);
|
||||
return ctx
|
||||
.replyWithMarkdown(emailText(mail, s), emailKb(s))
|
||||
.then(() => ctx.answerCbQuery())
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
bot.action('email5', (ctx) => {
|
||||
const s = state(ctx.from.id);
|
||||
const rows = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const mail = makeEmail(s);
|
||||
addHistory(ctx.from.id, '📧 ' + mail);
|
||||
rows.push(i + 1 + '. ' + BT + mail + BT);
|
||||
}
|
||||
return ctx
|
||||
.replyWithMarkdown(
|
||||
'🎰 Пачка из 5 адресов (' + EMAIL_DOMAINS[s.emailDomain] + ', ' + EMAIL_STYLES[s.emailStyle] + '):\n\n' + rows.join('\n'),
|
||||
Markup.inlineKeyboard([
|
||||
[Markup.button.callback('🔁 Ещё пачку', 'email5')],
|
||||
[Markup.button.callback('⬅️ Назад', 'email'), Markup.button.callback('🏠 Меню', 'main')],
|
||||
])
|
||||
)
|
||||
.then(() => ctx.answerCbQuery())
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
bot.action(/^edom:(-?\d+)$/, (ctx) => {
|
||||
const delta = Number(ctx.match[1]);
|
||||
const s = state(ctx.from.id);
|
||||
s.emailDomain = (s.emailDomain + delta + EMAIL_DOMAINS.length) % EMAIL_DOMAINS.length;
|
||||
return ctx
|
||||
.editMessageText('📧 Настройки адреса — жми «Ещё раз»:', emailKb(s))
|
||||
.then(() => ctx.answerCbQuery(EMAIL_DOMAINS[s.emailDomain]))
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
bot.action('estyle', (ctx) => {
|
||||
const s = state(ctx.from.id);
|
||||
s.emailStyle = (s.emailStyle + 1) % EMAIL_STYLES.length;
|
||||
return ctx
|
||||
.editMessageText('📧 Настройки адреса — жми «Ещё раз»:', emailKb(s))
|
||||
.then(() => ctx.answerCbQuery(EMAIL_STYLES[s.emailStyle]))
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
bot.action('enum', (ctx) => {
|
||||
const s = state(ctx.from.id);
|
||||
s.emailNumber = !s.emailNumber;
|
||||
return ctx
|
||||
.editMessageText('📧 Настройки адреса — жми «Ещё раз»:', emailKb(s))
|
||||
.then(() => ctx.answerCbQuery())
|
||||
.catch(() => {});
|
||||
});
|
||||
bot.action('history', (ctx) => {
|
||||
const h = history.get(ctx.from.id) || [];
|
||||
const text = h.length
|
||||
@@ -242,6 +336,7 @@ bot.action('about', (ctx) =>
|
||||
'📏 Длина 4–128 символов.\n' +
|
||||
'🔤 Наборы: A-Z, a-z, 0-9, символы; опции «без похожих» и «произносимый».\n' +
|
||||
'📊 Оценка стойкости в битах энтропии.\n' +
|
||||
'📧 Почтовые адреса: 3 стиля, 14 доменов (gmail, mail.ru, yandex, proton…).\n' +
|
||||
'🔒 Пароли не отправляются на серверы и не сохраняются на диске.',
|
||||
backKb()
|
||||
)
|
||||
@@ -265,6 +360,13 @@ bot.command('gen', (ctx) => {
|
||||
return sendPassword(ctx, s).catch(() => {});
|
||||
});
|
||||
|
||||
bot.command('email', (ctx) => {
|
||||
const s = state(ctx.from.id);
|
||||
const mail = makeEmail(s);
|
||||
addHistory(ctx.from.id, '📧 ' + mail);
|
||||
return ctx.replyWithMarkdown(emailText(mail, s), emailKb(s)).catch(() => {});
|
||||
});
|
||||
|
||||
bot.on('text', (ctx) => ctx.reply('Используй /start 🙂', mainMenu()));
|
||||
|
||||
bot.catch((err) => {
|
||||
@@ -277,6 +379,7 @@ bot.catch((err) => {
|
||||
const COMMANDS = [
|
||||
{ command: 'start', description: 'Главное меню' },
|
||||
{ command: 'gen', description: 'Сгенерировать пароль (/gen 24)' },
|
||||
{ command: 'email', description: 'Сгенерировать почтовый адрес' },
|
||||
{ command: 'help', description: 'Справка' },
|
||||
];
|
||||
|
||||
|
||||
@@ -100,3 +100,39 @@ export function strength(password) {
|
||||
|
||||
return { bits, level, bar };
|
||||
}
|
||||
|
||||
/* ---------- Email (почтовые адреса) ---------- */
|
||||
export const EMAIL_DOMAINS = [
|
||||
'gmail.com','googlemail.com','mail.ru','yandex.ru','ya.ru','outlook.com',
|
||||
'hotmail.com','yahoo.com','icloud.com','proton.me','protonmail.com','bk.ru','inbox.ru','list.ru',
|
||||
];
|
||||
|
||||
const EMAIL_WORDS = [
|
||||
'swift','dark','cyber','frost','neon','lunar','solar','storm','raven','wolf',
|
||||
'fox','pixel','turbo','quantum','echo','vortex','nova','pulse','drift','orbit',
|
||||
'lynx','onyx','zephyr','flux','ember','cobalt','delta','omega','prime','crux',
|
||||
];
|
||||
|
||||
const LOCAL_RANDOM = 'abcdefghijkmnpqrstuvwxyz';
|
||||
|
||||
/**
|
||||
* Стили local-части: random | pronounceable | wordnum
|
||||
* @param {object} opts { style, domain, withNumber, length }
|
||||
*/
|
||||
export function generateEmail(opts = {}) {
|
||||
const { style = 'random', domain = 'gmail.com', withNumber = true, length = 8 } = opts;
|
||||
const n = Math.min(Math.max(Number(length) || 8, 4), 24);
|
||||
|
||||
let local;
|
||||
if (style === 'pronounceable') {
|
||||
local = pronounceablePassword(n).toLowerCase();
|
||||
} else if (style === 'wordnum') {
|
||||
local = pick(EMAIL_WORDS) + '.' + pick(EMAIL_WORDS) + String(crypto.randomInt(10, 100));
|
||||
} else {
|
||||
local = Array.from({ length: n }, () => pick(LOCAL_RANDOM)).join('');
|
||||
}
|
||||
|
||||
if (withNumber && !new RegExp('[0-9]').test(local)) local += String(crypto.randomInt(10, 1000));
|
||||
const dom = EMAIL_DOMAINS.includes(domain) ? domain : EMAIL_DOMAINS[0];
|
||||
return local.toLowerCase() + '@' + dom;
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "node --watch index.js",
|
||||
"test": "node tests/smoke.mjs",
|
||||
"test": "node tests/smoke.mjs && node tests/email.mjs",
|
||||
"docker": "docker build -t tg_bot_gen ."
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { generateEmail, EMAIL_DOMAINS } from '../lib/generator.js';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
|
||||
for (const style of ['random', 'pronounceable', 'wordnum']) {
|
||||
for (const domain of EMAIL_DOMAINS) {
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const e = generateEmail({ style, domain, withNumber: i % 2 === 0 });
|
||||
assert.match(e, /^[a-z0-9][a-z0-9.]*@[a-z.]+$/, 'format for ' + style + '/' + domain + ' -> ' + e);
|
||||
assert.ok(e.endsWith('@' + domain), 'domain kept: ' + e);
|
||||
const local = e.split('@')[0];
|
||||
assert.ok(local.length >= 5, 'local length ok: ' + e);
|
||||
if (i % 2 === 0) assert.match(local, /\d/, 'has number: ' + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('EMAIL SMOKE OK — ' + EMAIL_DOMAINS.length * 3 * 200 + ' адресов валидны');
|
||||
Reference in new issue
Block a user