Add pack of 5, /gen command, command menu, update logging, telegraf v4 API fixes

This commit is contained in:
kromskii2 committed 2026-09-10 14:48:31 +07:00
1 parent c76676713f
commit 3ba969dbf4
2 files changed
+135 -50

No files matched your search

+2
View File
@@ -15,3 +15,5 @@ npm-debug.log*
Thumbs.db Thumbs.db
.vscode/ .vscode/
.idea/ .idea/
bot.log
+133 -50
View File
@@ -1,7 +1,14 @@
import 'dotenv/config'; import 'dotenv/config';
import fs from 'node:fs';
import { Telegraf, Markup } from 'telegraf'; import { Telegraf, Markup } from 'telegraf';
import { generatePassword, strength } from './lib/generator.js'; 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 TOKEN = process.env.BOT_TOKEN;
const OWNER_ID = Number(process.env.OWNER_ID); const OWNER_ID = Number(process.env.OWNER_ID);
@@ -41,6 +48,7 @@ const addHistory = (id, pwd) => {
const mainMenu = () => const mainMenu = () =>
Markup.inlineKeyboard([ Markup.inlineKeyboard([
[Markup.button.callback('🔑 Сгенерировать пароль', 'gen')], [Markup.button.callback('🔑 Сгенерировать пароль', 'gen')],
[Markup.button.callback('🎰 Пачка из 5', 'gen5')],
[Markup.button.callback('⚙️ Настройки', 'settings')], [Markup.button.callback('⚙️ Настройки', 'settings')],
[Markup.button.callback('📋 История', 'history')], [Markup.button.callback('📋 История', 'history')],
[Markup.button.callback('ℹ️ О боте', 'about')], [Markup.button.callback('ℹ️ О боте', 'about')],
@@ -49,13 +57,11 @@ const mainMenu = () =>
const settingsKb = (s) => { const settingsKb = (s) => {
const on = (v) => (v ? '✅' : '❎'); const on = (v) => (v ? '✅' : '❎');
return Markup.inlineKeyboard([ 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('➖ 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.upper)} Заглавные (A-Z)`, 'set:upper')],
@@ -70,27 +76,13 @@ const settingsKb = (s) => {
const resultKb = () => const resultKb = () =>
Markup.inlineKeyboard([ Markup.inlineKeyboard([
[Markup.button.callback('🔁 Ещё раз', 'gen')], [Markup.button.callback('🔁 Ещё раз', 'gen'), Markup.button.callback('🎰 Пачка из 5', 'gen5')],
[Markup.button.callback('⚙️ Настройки', 'settings'), Markup.button.callback('⬅️ Меню', 'main')], [Markup.button.callback('⚙️ Настройки', 'settings'), Markup.button.callback('⬅️ Меню', 'main')],
]); ]);
const backKb = (cb = 'main') => const backKb = (cb = 'main') =>
Markup.inlineKeyboard([[Markup.button.callback('⬅️ Назад', cb)]]); 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) => { const modeLabel = (s) => {
if (s.pronounceable) return 'произносимый'; if (s.pronounceable) return 'произносимый';
const parts = []; const parts = [];
@@ -101,34 +93,68 @@ const modeLabel = (s) => {
return parts.join(' + ') || 'a-z'; 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 ---------- */ /* ---------- Access restriction: OWNER_ID only ---------- */
const ownerOnly = async (ctx, next) => { const ownerOnly = async (ctx, next) => {
if (!Number.isFinite(OWNER_ID) || ctx.from?.id === OWNER_ID) return next(); if (!Number.isFinite(OWNER_ID) || ctx.from?.id === OWNER_ID) return next();
if (ctx.callbackQuery) await ctx.answerCbQuery('Доступ запрещен').catch(() => {}); if (ctx.callbackQuery) await ctx.answerCbQuery('Доступ запрещен').catch(() => {});
return ctx.reply('⛔ Доступ разрешен только владельцу бота.').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); bot.use(ownerOnly);
/* ---------- Handlers ---------- */ /* ---------- Handlers ---------- */
bot.start((ctx) => { bot.start((ctx) => {
state(ctx.from.id); state(ctx.from.id);
return ctx.reply( return sendGreeting(ctx);
'👋 Привет! Я генератор паролей.\n\n' +
'🔑 Жми «Сгенерировать» — пароль создаётся криптографически стойким генератором Node.js (crypto.randomInt).\n' +
'⚙️ В настройках — длина, наборы символов, режимы.',
mainMenu()
);
}); });
bot.action('noop', (ctx) => ctx.answerCbQuery()); bot.action('noop', (ctx) => ctx.answerCbQuery());
bot.action('main', (ctx) => bot.action('main', (ctx) => {
ctx.editMessageText('Выбери действие:', mainMenu()).catch(() => {}) ctx.answerCbQuery().catch(() => {});
); return ctx.editMessageText('Выбери действие:', mainMenu()).catch(() => {});
});
bot.action('settings', (ctx) => { bot.action('settings', (ctx) => {
const s = state(ctx.from.id); 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) => { bot.action(/^len:(-?\d+)$/, (ctx) => {
@@ -136,7 +162,7 @@ bot.action(/^len:(-?\d+)$/, (ctx) => {
const s = state(ctx.from.id); const s = state(ctx.from.id);
s.length = Math.min(128, Math.max(4, s.length + delta)); s.length = Math.min(128, Math.max(4, s.length + delta));
return ctx return ctx
.editMessageText(`⚙️ Настройки генерации:\nДлина: ${s.length} (4–128)`, settingsKb(s)) .editMessageText('⚙️ Настройки генерации:', settingsKb(s))
.then(() => ctx.answerCbQuery(`Длина: ${s.length}`)) .then(() => ctx.answerCbQuery(`Длина: ${s.length}`))
.catch(() => {}); .catch(() => {});
}); });
@@ -154,15 +180,35 @@ bot.action(/^set:(\w+)$/, (ctx) => {
} }
s[key] = !s[key]; s[key] = !s[key];
} }
return ctx.editMessageText('⚙️ Настройки генерации:', settingsKb(s)).catch(() => {}); return ctx
.editMessageText('⚙️ Настройки генерации:', settingsKb(s))
.then(() => ctx.answerCbQuery())
.catch(() => {});
}); });
bot.action('gen', async (ctx) => { bot.action('gen', async (ctx) => {
const s = state(ctx.from.id); const s = state(ctx.from.id);
const pwd = generatePassword(s);
addHistory(ctx.from.id, pwd);
await ctx.answerCbQuery('Готово').catch(() => {}); 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) => { bot.action('history', (ctx) => {
@@ -174,12 +220,18 @@ bot.action('history', (ctx) => {
[Markup.button.callback('🧹 Очистить', 'clearhist')], [Markup.button.callback('🧹 Очистить', 'clearhist')],
[Markup.button.callback('⬅️ Назад', 'main')], [Markup.button.callback('⬅️ Назад', 'main')],
]); ]);
return ctx.replyWithMarkdown(text, kb).catch(() => {}); return ctx
.replyWithMarkdown(text, kb)
.then(() => ctx.answerCbQuery())
.catch(() => {});
}); });
bot.action('clearhist', (ctx) => { bot.action('clearhist', (ctx) => {
history.delete(ctx.from.id); history.delete(ctx.from.id);
return ctx.editMessageText('🧹 История очищена.', backKb()).catch(() => {}); return ctx
.editMessageText('🧹 История очищена.', backKb())
.then(() => ctx.answerCbQuery())
.catch(() => {});
}); });
bot.action('about', (ctx) => bot.action('about', (ctx) =>
@@ -189,32 +241,63 @@ bot.action('about', (ctx) =>
'🛡 Node.js crypto (crypto.randomInt), Fisher–Yates перемешивание, гарантия минимум одного символа из каждого набора.\n' + '🛡 Node.js crypto (crypto.randomInt), Fisher–Yates перемешивание, гарантия минимум одного символа из каждого набора.\n' +
'📏 Длина 4–128 символов.\n' + '📏 Длина 4–128 символов.\n' +
'🔤 Наборы: A-Z, a-z, 0-9, символы; опции «без похожих» и «произносимый».\n' + '🔤 Наборы: A-Z, a-z, 0-9, символы; опции «без похожих» и «произносимый».\n' +
'📊 Оценка стойкости в битах энтропии.\n' +
'🔒 Пароли не отправляются на серверы и не сохраняются на диске.', '🔒 Пароли не отправляются на серверы и не сохраняются на диске.',
backKb() backKb()
) )
.then(() => ctx.answerCbQuery())
.catch(() => {}) .catch(() => {})
); );
bot.on(['text', 'message'], (ctx) => { // Быстрые команды: /help /menu /gen <length>
const t = ctx.message?.text; bot.help((ctx) => sendGreeting(ctx));
if (t === '/start' || t === '/help' || t === '/menu') return bot.start(ctx); bot.command('menu', (ctx) => sendGreeting(ctx));
return ctx.reply('Используй /start 🙂', mainMenu()); 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) ---------- */ /* ---------- Start (long polling) ---------- */
bot boot('booting...');
.launch({ dropPendingUpdates: true }) bot.telegram.getMe().then((me) => boot('getMe ok @' + me.username)).catch((e) => boot('getMe FAIL ' + (e?.response?.description || e.message)));
.then(() => console.log(`Bot @${bot.botInfo?.username ?? '?'} started (polling)`))
.catch((e) => { bot.telegram
console.error('Failed to launch:', e?.response?.description || e.message); .setMyCommands(COMMANDS)
process.exit(1); .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 = () => { const shutdown = () => {
bot.stop('shutting down'); boot('stopping');
try { bot.stop('shutting down'); } catch {}
process.exit(0); process.exit(0);
}; };
process.once('SIGINT', shutdown); process.once('SIGINT', shutdown);
process.once('SIGTERM', shutdown); process.once('SIGTERM', shutdown);
process.on('unhandledRejection', (e) => boot('UNHANDLED: ' + (e?.message || e)));