commit c76676713f5ac5bf470592a1c01dc1409ce402fb Author: kromskii2 Date: Thu Sep 10 14:30:10 2026 +0700 Initial commit: Telegram password generator bot (telegraf, inline menu, crypto.randomInt) diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2d77f02 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +BOT_TOKEN=your-bot-token-here +OWNER_ID=your-telegram-user-id diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84eef65 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Секреты — никогда не коммитим +.env +.env.* +!.env.example + +# Зависимости +node_modules/ + +# Логи +*.log +npm-debug.log* + +# ОС / редакторы +.DS_Store +Thumbs.db +.vscode/ +.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc10ea4 --- /dev/null +++ b/README.md @@ -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 # генератор + оценка энтропии +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..4e3a2fd --- /dev/null +++ b/index.js @@ -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); diff --git a/lib/generator.js b/lib/generator.js new file mode 100644 index 0000000..46fab19 --- /dev/null +++ b/lib/generator.js @@ -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 }; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4181431 --- /dev/null +++ b/package-lock.json @@ -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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4ab45f2 --- /dev/null +++ b/package.json @@ -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" + } +}