From 3ba969dbf44506e7bae98059315fea879fa5bdf8 Mon Sep 17 00:00:00 2001 From: kromskii2 Date: Thu, 10 Sep 2026 14:48:31 +0700 Subject: [PATCH] Add pack of 5, /gen command, command menu, update logging, telegraf v4 API fixes --- .gitignore | 2 + index.js | 183 ++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 135 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index 84eef65..3c40c2b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ npm-debug.log* Thumbs.db .vscode/ .idea/ + +bot.log diff --git a/index.js b/index.js index 4e3a2fd..8ec4ea7 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,14 @@ import 'dotenv/config'; +import fs from 'node:fs'; import { Telegraf, Markup } from 'telegraf'; import { generatePassword, strength } from './lib/generator.js'; +const boot = (m) => + fs.appendFileSync( + new URL('./bot.log', import.meta.url), + new Date().toISOString() + ' ' + m + '\n' + ); + const TOKEN = process.env.BOT_TOKEN; const OWNER_ID = Number(process.env.OWNER_ID); @@ -41,6 +48,7 @@ const addHistory = (id, pwd) => { const mainMenu = () => Markup.inlineKeyboard([ [Markup.button.callback('🔑 Сгенерировать пароль', 'gen')], + [Markup.button.callback('🎰 Пачка из 5', 'gen5')], [Markup.button.callback('⚙️ Настройки', 'settings')], [Markup.button.callback('📋 История', 'history')], [Markup.button.callback('ℹ️ О боте', 'about')], @@ -49,13 +57,11 @@ const mainMenu = () => 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('➖ 1', 'len:-1'), + Markup.button.callback(`📏 ${s.length}`, 'noop'), + Markup.button.callback('➕ 1', 'len:+1'), Markup.button.callback('➕ 5', 'len:+5'), ], [Markup.button.callback(`${on(s.upper)} Заглавные (A-Z)`, 'set:upper')], @@ -70,27 +76,13 @@ const settingsKb = (s) => { const resultKb = () => Markup.inlineKeyboard([ - [Markup.button.callback('🔁 Ещё раз', 'gen')], + [Markup.button.callback('🔁 Ещё раз', 'gen'), Markup.button.callback('🎰 Пачка из 5', 'gen5')], [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 = []; @@ -101,34 +93,68 @@ const modeLabel = (s) => { return parts.join(' + ') || 'a-z'; }; +const passwordText = (pwd, s) => { + const st = strength(pwd); + return [ + '🔑 Ваш пароль:', + '', + '`' + pwd + '`', + '', + `${st.bar} ${st.level} · ~${st.bits} бит энтропии`, + `⚙️ Длина ${s.length} · ${modeLabel(s)}`, + '', + '🛡 crypto.randomInt — нажми на пароль, чтобы скопировать.', + ].join('\n'); +}; + +/* ---------- Shared actions ---------- */ +const sendPassword = async (ctx, s) => { + const pwd = generatePassword(s); + addHistory(ctx.from.id, pwd); + return ctx.replyWithMarkdown(passwordText(pwd, s), resultKb()); +}; + +const sendGreeting = (ctx) => + ctx.reply( + '👋 Привет! Я генератор паролей.\n\n' + + '🔑 Жми «Сгенерировать» — пароль создаётся криптографически стойким генератором Node.js (crypto.randomInt).\n' + + '⚙️ В настройках — длина (4–128), наборы символов, режимы.\n' + + '💬 Быстрая команда: /gen 24 — пароль длиной 24.', + mainMenu() + ); + /* ---------- 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(async (ctx, next) => { + const t = ctx.callbackQuery ? "callback " + ctx.callbackQuery.data : "message " + (ctx.message?.text || ""); + boot("UPDATE from " + ctx.from?.id + ": " + t); + return next(); +}); bot.use(ownerOnly); /* ---------- Handlers ---------- */ bot.start((ctx) => { state(ctx.from.id); - return ctx.reply( - '👋 Привет! Я генератор паролей.\n\n' + - '🔑 Жми «Сгенерировать» — пароль создаётся криптографически стойким генератором Node.js (crypto.randomInt).\n' + - '⚙️ В настройках — длина, наборы символов, режимы.', - mainMenu() - ); + return sendGreeting(ctx); }); bot.action('noop', (ctx) => ctx.answerCbQuery()); -bot.action('main', (ctx) => - ctx.editMessageText('Выбери действие:', mainMenu()).catch(() => {}) -); +bot.action('main', (ctx) => { + ctx.answerCbQuery().catch(() => {}); + return ctx.editMessageText('Выбери действие:', mainMenu()).catch(() => {}); +}); bot.action('settings', (ctx) => { const s = state(ctx.from.id); - return ctx.editMessageText('⚙️ Настройки генерации:', settingsKb(s)).catch(() => {}); + return ctx + .editMessageText('⚙️ Настройки генерации:', settingsKb(s)) + .then(() => ctx.answerCbQuery()) + .catch(() => {}); }); bot.action(/^len:(-?\d+)$/, (ctx) => { @@ -136,7 +162,7 @@ bot.action(/^len:(-?\d+)$/, (ctx) => { 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)) + .editMessageText('⚙️ Настройки генерации:', settingsKb(s)) .then(() => ctx.answerCbQuery(`Длина: ${s.length}`)) .catch(() => {}); }); @@ -154,15 +180,35 @@ bot.action(/^set:(\w+)$/, (ctx) => { } s[key] = !s[key]; } - return ctx.editMessageText('⚙️ Настройки генерации:', settingsKb(s)).catch(() => {}); + return ctx + .editMessageText('⚙️ Настройки генерации:', settingsKb(s)) + .then(() => ctx.answerCbQuery()) + .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(() => {}); + return sendPassword(ctx, s).catch(() => {}); +}); + +bot.action('gen5', async (ctx) => { + const s = state(ctx.from.id); + const rows = []; + for (let i = 0; i < 5; i++) { + const pwd = generatePassword(s); + addHistory(ctx.from.id, pwd); + const st = strength(pwd); + rows.push(`${i + 1}. \`${pwd}\` · ${st.bar}`); + } + const text = `🎰 Пачка из 5 (длина ${s.length}, ${modeLabel(s)}):\n\n` + rows.join('\n'); + await ctx.answerCbQuery('Готово').catch(() => {}); + return ctx + .replyWithMarkdown(text, Markup.inlineKeyboard([ + [Markup.button.callback('🔁 Ещё пачку', 'gen5')], + [Markup.button.callback('⚙️ Настройки', 'settings'), Markup.button.callback('⬅️ Меню', 'main')], + ])) + .catch(() => {}); }); bot.action('history', (ctx) => { @@ -174,12 +220,18 @@ bot.action('history', (ctx) => { [Markup.button.callback('🧹 Очистить', 'clearhist')], [Markup.button.callback('⬅️ Назад', 'main')], ]); - return ctx.replyWithMarkdown(text, kb).catch(() => {}); + return ctx + .replyWithMarkdown(text, kb) + .then(() => ctx.answerCbQuery()) + .catch(() => {}); }); bot.action('clearhist', (ctx) => { history.delete(ctx.from.id); - return ctx.editMessageText('🧹 История очищена.', backKb()).catch(() => {}); + return ctx + .editMessageText('🧹 История очищена.', backKb()) + .then(() => ctx.answerCbQuery()) + .catch(() => {}); }); bot.action('about', (ctx) => @@ -189,32 +241,63 @@ bot.action('about', (ctx) => '🛡 Node.js crypto (crypto.randomInt), Fisher–Yates перемешивание, гарантия минимум одного символа из каждого набора.\n' + '📏 Длина 4–128 символов.\n' + '🔤 Наборы: A-Z, a-z, 0-9, символы; опции «без похожих» и «произносимый».\n' + + '📊 Оценка стойкости в битах энтропии.\n' + '🔒 Пароли не отправляются на серверы и не сохраняются на диске.', backKb() ) + .then(() => ctx.answerCbQuery()) .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()); +// Быстрые команды: /help /menu /gen +bot.help((ctx) => sendGreeting(ctx)); +bot.command('menu', (ctx) => sendGreeting(ctx)); +bot.command('gen', (ctx) => { + const arg = (ctx.message?.text || '').trim().split(/\s+/)[1]; + const s = state(ctx.from.id); + if (arg) { + const n = Number(arg); + if (!Number.isFinite(n) || n < 4 || n > 128) { + return ctx.reply('📏 Длина — число от 4 до 128. Пример: /gen 24'); + } + s.length = Math.round(n); + } + return sendPassword(ctx, s).catch(() => {}); }); -bot.catch((err) => console.error('Bot error:', err?.response?.description || err)); +bot.on('text', (ctx) => ctx.reply('Используй /start 🙂', mainMenu())); + +bot.catch((err) => { + const msg = err?.response?.description || err?.message || String(err); + boot('HANDLER ERROR: ' + msg); + console.error('Bot error:', msg); +}); + +/* ---------- Menu of bot commands ---------- */ +const COMMANDS = [ + { command: 'start', description: 'Главное меню' }, + { command: 'gen', description: 'Сгенерировать пароль (/gen 24)' }, + { command: 'help', description: 'Справка' }, +]; /* ---------- 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); - }); +boot('booting...'); +bot.telegram.getMe().then((me) => boot('getMe ok @' + me.username)).catch((e) => boot('getMe FAIL ' + (e?.response?.description || e.message))); + +bot.telegram + .setMyCommands(COMMANDS) + .then(() => boot('setMyCommands OK')) + .catch((e) => boot('setMyCommands FAIL ' + (e?.response?.description || e.message))); + +boot('launching...'); +bot.launch({ dropPendingUpdates: true }).finally(() => boot('polling stopped')); +console.log('Bot @' + TOKEN.slice(0,4) + ' launched; waiting for updates'); const shutdown = () => { - bot.stop('shutting down'); + boot('stopping'); + try { bot.stop('shutting down'); } catch {} process.exit(0); }; process.once('SIGINT', shutdown); process.once('SIGTERM', shutdown); +process.on('unhandledRejection', (e) => boot('UNHANDLED: ' + (e?.message || e)));