Initial commit: Telegram password generator bot (telegraf, inline menu, crypto.randomInt)

This commit is contained in:
kromskii2 committed 2026-09-10 14:30:10 +07:00
commit c76676713f
7 files changed
+607

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
BOT_TOKEN=your-bot-token-here
OWNER_ID=your-telegram-user-id
+17
View File
@@ -0,0 +1,17 @@
# Секреты — никогда не коммитим
.env
.env.*
!.env.example
# Зависимости
node_modules/
# Логи
*.log
npm-debug.log*
# ОС / редакторы
.DS_Store
Thumbs.db
.vscode/
.idea/
+37
View File
@@ -0,0 +1,37 @@
# tg_bot_gen
Telegram-бот — полноценный генератор паролей с инлайн-меню (telegraf v4, Node.js).
## Возможности
- `/start` — всё меню на инлайн-кнопках
- 🔑 Генерация паролей через `crypto.randomInt` (без `Math.random`)
- ⚙️ Настройки на кнопках:
- длина 4–128 (кнопки −5 / −1 / +1 / +5)
- наборы: A-Z, a-z, 0-9, символы
- «без похожих символов» (il1Lo0O)
- «произносимый» режим (CV-паттерн)
- 📊 Оценка стойкости: энтропия в битах + уровень
- 📋 История последних 5 паролей (в памяти, на диск не пишется)
- 🔒 Доступ только для владельца (`OWNER_ID` из `.env`)
## Запуск
```bash
npm install
npm start # или npm run dev (с --watch)
```
## Настройка `.env`
```
BOT_TOKEN=***
OWNER_ID=ваш_user_id
```
## Структура
```
index.js # бот: меню, хэндлеры кнопок
lib/generator.js # генератор + оценка энтропии
```
+220
View File
@@ -0,0 +1,220 @@
import 'dotenv/config';
import { Telegraf, Markup } from 'telegraf';
import { generatePassword, strength } from './lib/generator.js';
const TOKEN = process.env.BOT_TOKEN;
const OWNER_ID = Number(process.env.OWNER_ID);
if (!TOKEN) {
console.error('BOT_TOKEN not set in .env');
process.exit(1);
}
const bot = new Telegraf(TOKEN, { handlerTimeout: 5_000 });
/* ---------- User settings state (in-memory) ---------- */
const defaults = () => ({
length: 16,
upper: true,
lower: true,
digits: true,
symbols: true,
excludeAmbiguous: false,
pronounceable: false,
});
const users = new Map();
const history = new Map(); // userId -> [passwords]
const state = (id) => {
if (!users.has(id)) users.set(id, defaults());
return users.get(id);
};
const addHistory = (id, pwd) => {
if (!history.has(id)) history.set(id, []);
const h = history.get(id);
h.unshift(pwd);
if (h.length > 5) h.pop();
};
/* ---------- Keyboard builders ---------- */
const mainMenu = () =>
Markup.inlineKeyboard([
[Markup.button.callback('🔑 Сгенерировать пароль', 'gen')],
[Markup.button.callback('⚙️ Настройки', 'settings')],
[Markup.button.callback('📋 История', 'history')],
[Markup.button.callback('ℹ️ О боте', 'about')],
]);
const settingsKb = (s) => {
const on = (v) => (v ? '✅' : '❎');
return Markup.inlineKeyboard([
[
Markup.button.callback('➖ 1', 'len:-1'),
Markup.button.callback(`📏 Длина: ${s.length}`, 'noop'),
Markup.button.callback('➕ 1', 'len:+1'),
],
[
Markup.button.callback('➖ 5', 'len:-5'),
Markup.button.callback('➕ 5', 'len:+5'),
],
[Markup.button.callback(`${on(s.upper)} Заглавные (A-Z)`, 'set:upper')],
[Markup.button.callback(`${on(s.lower)} Строчные (a-z)`, 'set:lower')],
[Markup.button.callback(`${on(s.digits)} Цифры (0-9)`, 'set:digits')],
[Markup.button.callback(`${on(s.symbols)} Символы (!@#$)`, 'set:symbols')],
[Markup.button.callback(`${on(s.excludeAmbiguous)} Без похожих (il1Lo0O)`, 'set:excludeAmbiguous')],
[Markup.button.callback(`${on(s.pronounceable)} Произносимый`, 'set:pronounceable')],
[Markup.button.callback('🔑 Сгенерировать', 'gen'), Markup.button.callback('⬅️ Назад', 'main')],
]);
};
const resultKb = () =>
Markup.inlineKeyboard([
[Markup.button.callback('🔁 Ещё раз', 'gen')],
[Markup.button.callback('⚙️ Настройки', 'settings'), Markup.button.callback('⬅️ Меню', 'main')],
]);
const backKb = (cb = 'main') =>
Markup.inlineKeyboard([[Markup.button.callback('⬅️ Назад', cb)]]);
const passwordText = (pwd, s) => {
const st = strength(pwd);
return [
'🔑 Ваш пароль:',
'',
'`' + pwd + '`',
'',
`${st.bar} ${st.level} · ~${st.bits} бит энтропии`,
`⚙️ Длина ${s.length} · ${modeLabel(s)}`,
'',
'🛡 crypto.randomInt, без Math.random.',
].join('\n');
};
const modeLabel = (s) => {
if (s.pronounceable) return 'произносимый';
const parts = [];
if (s.upper) parts.push('A-Z');
if (s.lower) parts.push('a-z');
if (s.digits) parts.push('0-9');
if (s.symbols) parts.push('!@#');
return parts.join(' + ') || 'a-z';
};
/* ---------- Access restriction: OWNER_ID only ---------- */
const ownerOnly = async (ctx, next) => {
if (!Number.isFinite(OWNER_ID) || ctx.from?.id === OWNER_ID) return next();
if (ctx.callbackQuery) await ctx.answerCbQuery('Доступ запрещен').catch(() => {});
return ctx.reply('⛔ Доступ разрешен только владельцу бота.').catch(() => {});
};
bot.use(ownerOnly);
/* ---------- Handlers ---------- */
bot.start((ctx) => {
state(ctx.from.id);
return ctx.reply(
'👋 Привет! Я генератор паролей.\n\n' +
'🔑 Жми «Сгенерировать» — пароль создаётся криптографически стойким генератором Node.js (crypto.randomInt).\n' +
'⚙️ В настройках — длина, наборы символов, режимы.',
mainMenu()
);
});
bot.action('noop', (ctx) => ctx.answerCbQuery());
bot.action('main', (ctx) =>
ctx.editMessageText('Выбери действие:', mainMenu()).catch(() => {})
);
bot.action('settings', (ctx) => {
const s = state(ctx.from.id);
return ctx.editMessageText('⚙️ Настройки генерации:', settingsKb(s)).catch(() => {});
});
bot.action(/^len:(-?\d+)$/, (ctx) => {
const delta = Number(ctx.match[1]);
const s = state(ctx.from.id);
s.length = Math.min(128, Math.max(4, s.length + delta));
return ctx
.editMessageText(`⚙️ Настройки генерации:\nДлина: ${s.length} (4–128)`, settingsKb(s))
.then(() => ctx.answerCbQuery(`Длина: ${s.length}`))
.catch(() => {});
});
bot.action(/^set:(\w+)$/, (ctx) => {
const key = ctx.match[1];
if (!(key in defaults())) return ctx.answerCbQuery('Неизвестная настройка');
const s = state(ctx.from.id);
if (key === 'pronounceable' || key === 'excludeAmbiguous') {
s[key] = !s[key];
} else {
const others = ['upper', 'lower', 'digits', 'symbols'].filter((k) => k !== key);
if (s[key] && !others.some((k) => s[k])) {
return ctx.answerCbQuery('Нужен хотя бы один набор символов');
}
s[key] = !s[key];
}
return ctx.editMessageText('⚙️ Настройки генерации:', settingsKb(s)).catch(() => {});
});
bot.action('gen', async (ctx) => {
const s = state(ctx.from.id);
const pwd = generatePassword(s);
addHistory(ctx.from.id, pwd);
await ctx.answerCbQuery('Готово').catch(() => {});
return ctx.replyWithMarkdown(passwordText(pwd, s), resultKb()).catch(() => {});
});
bot.action('history', (ctx) => {
const h = history.get(ctx.from.id) || [];
const text = h.length
? '📋 Последние 5 паролей:\n\n' + h.map((p, i) => `${i + 1}. \`${p}\``).join('\n')
: '📋 История пуста — сначала сгенерируй пароль.';
const kb = Markup.inlineKeyboard([
[Markup.button.callback('🧹 Очистить', 'clearhist')],
[Markup.button.callback('⬅️ Назад', 'main')],
]);
return ctx.replyWithMarkdown(text, kb).catch(() => {});
});
bot.action('clearhist', (ctx) => {
history.delete(ctx.from.id);
return ctx.editMessageText('🧹 История очищена.', backKb()).catch(() => {});
});
bot.action('about', (ctx) =>
ctx
.editMessageText(
'ℹ️ tg_bot_gen — генератор паролей в Telegram\n\n' +
'🛡 Node.js crypto (crypto.randomInt), Fisher–Yates перемешивание, гарантия минимум одного символа из каждого набора.\n' +
'📏 Длина 4–128 символов.\n' +
'🔤 Наборы: A-Z, a-z, 0-9, символы; опции «без похожих» и «произносимый».\n' +
'🔒 Пароли не отправляются на серверы и не сохраняются на диске.',
backKb()
)
.catch(() => {})
);
bot.on(['text', 'message'], (ctx) => {
const t = ctx.message?.text;
if (t === '/start' || t === '/help' || t === '/menu') return bot.start(ctx);
return ctx.reply('Используй /start 🙂', mainMenu());
});
bot.catch((err) => console.error('Bot error:', err?.response?.description || err));
/* ---------- Start (long polling) ---------- */
bot
.launch({ dropPendingUpdates: true })
.then(() => console.log(`Bot @${bot.botInfo?.username ?? '?'} started (polling)`))
.catch((e) => {
console.error('Failed to launch:', e?.response?.description || e.message);
process.exit(1);
});
const shutdown = () => {
bot.stop('shutting down');
process.exit(0);
};
process.once('SIGINT', shutdown);
process.once('SIGTERM', shutdown);
+102
View File
@@ -0,0 +1,102 @@
// Генератор паролей на crypto API (cryptographically secure)
import crypto from 'node:crypto';
const SETS = {
upper: 'ABCDEFGHJKLMNPQRSTUVWXYZ', // без I O (при excludeAmbiguous)
upperFull: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
lower: 'abcdefghijkmnpqrstuvwxyz', // без l o
lowerFull: 'abcdefghijklmnopqrstuvwxyz',
digits: '23456789', // без 0 1
digitsFull: '0123456789',
symbols: '!@#$%^&*()_+-=[]{};:,.<>?/~',
};
const VOWELS = 'aeiou';
const CONSONANTS = 'bcdfghjkmnpqrstvz';
function randInt(max) {
return crypto.randomIntRange ? crypto.randomIntRange(max) : crypto.randomInt(0, max);
}
function pick(str) {
return str[randInt(str.length)];
}
/**
* @param {object} opts
* @param {number} opts.length
* @param {boolean} opts.upper
* @param {boolean} opts.lower
* @param {boolean} opts.digits
* @param {boolean} opts.symbols
* @param {boolean} opts.excludeAmbiguous - убрать похожие символы (il1Lo0O)
* @param {boolean} opts.pronounceable - произносимый режим (CV-паттерн)
*/
export function generatePassword(opts) {
const {
length = 16,
upper = true,
lower = true,
digits = true,
symbols = true,
excludeAmbiguous = false,
pronounceable = false,
} = opts || {};
const len = Math.min(Math.max ? Math.max(Number(length) || 16, 4) : 16, 128);
if (pronounceable) return pronounceablePassword(len);
const active = [];
if (upper) active.push(excludeAmbiguous ? SETS.upper : SETS.upperFull);
if (lower) active.push(excludeAmbiguous ? SETS.lower : SETS.lowerFull);
if (digits) active.push(excludeAmbiguous ? SETS.digits : SETS.digitsFull);
if (symbols) active.push(SETS.symbols);
if (active.length === 0) active.push(SETS.lowerFull);
const pool = active.join('');
// Гарантируем минимум один символ из каждого выбранного набора
const chars = active.map(pick);
while (chars.length < len) chars.push(pick(pool));
// Перемешиваем (Fisher–Yates на crypto)
for (let i = chars.length - 1; i > 0; i--) {
const j = randInt(i + 1);
[chars[i], chars[j]] = [chars[j], chars[i]];
}
return chars.slice(0, len).join('');
}
function pronounceablePassword(len) {
const out = [];
while (out.length < len) {
const c = pick(CONSONANTS);
const v = pick(VOWELS);
out.push(randInt(2) === 0 ? c : c.toUpperCase(), v);
if (randInt(10) < 3 && out.length < len) out.push(pick('0123456789'));
}
return out.slice(0, len).join('');
}
/**
* Оценка пароля: энтропия (бит) + уровень.
*/
export function strength(password) {
let poolSize = 0;
if (/[a-z]/.test(password)) poolSize += 26;
if (/[A-Z]/.test(password)) poolSize += 26;
if (/[0-9]/.test(password)) poolSize += 10;
if (/[^a-zA-Z0-9]/.test(password)) poolSize += 32;
const bits = Math.round(password.length * Math.log2(poolSize || 1));
let level, bar;
if (bits < 40) { level = 'Слабый'; bar = '🟥⬜⬜⬜⬜'; }
else if (bits < 60) { level = 'Средний'; bar = '🟨🟨⬜⬜⬜'; }
else if (bits < 80) { level = 'Хороший'; bar = '🟩🟩🟩⬜⬜'; }
else if (bits < 120) { level = 'Отличный'; bar = '🟩🟩🟩🟩⬜'; }
else { level = 'Максимальный'; bar = '🟩🟩🟩🟩🟩'; }
return { bits, level, bar };
}
+204
View File
@@ -0,0 +1,204 @@
{
"name": "tg_bot_gen",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tg_bot_gen",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"dotenv": "^17.4.2",
"telegraf": "^4.16.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@telegraf/types": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@telegraf/types/-/types-7.1.0.tgz",
"integrity": "sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==",
"license": "MIT"
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/buffer-alloc": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
"integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
"license": "MIT",
"dependencies": {
"buffer-alloc-unsafe": "^1.1.0",
"buffer-fill": "^1.0.0"
}
},
"node_modules/buffer-alloc-unsafe": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
"license": "MIT"
},
"node_modules/buffer-fill": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
"integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/p-timeout": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-4.1.0.tgz",
"integrity": "sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/safe-compare": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/safe-compare/-/safe-compare-1.1.4.tgz",
"integrity": "sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==",
"license": "MIT",
"dependencies": {
"buffer-alloc": "^1.2.0"
}
},
"node_modules/sandwich-stream": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/sandwich-stream/-/sandwich-stream-2.0.2.tgz",
"integrity": "sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==",
"license": "Apache-2.0",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/telegraf": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.16.3.tgz",
"integrity": "sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==",
"license": "MIT",
"dependencies": {
"@telegraf/types": "^7.1.0",
"abort-controller": "^3.0.0",
"debug": "^4.3.4",
"mri": "^1.2.0",
"node-fetch": "^2.7.0",
"p-timeout": "^4.1.0",
"safe-compare": "^1.1.4",
"sandwich-stream": "^2.0.2"
},
"bin": {
"telegraf": "lib/cli.mjs"
},
"engines": {
"node": "^12.20.0 || >=14.13.1"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
}
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"name": "tg_bot_gen",
"version": "1.0.0",
"description": "Telegram bot: генератор паролей с инлайн-меню",
"main": "index.js",
"type": "module",
"scripts": {
"start": "node index.js",
"dev": "node --watch index.js"
},
"keywords": [
"telegram",
"bot",
"password",
"generator"
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"dependencies": {
"dotenv": "^17.4.2",
"telegraf": "^4.16.3"
}
}