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
.vscode/
.idea/
bot.log
+133 -50
View File
@@ -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 <length>
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)));