Add pack of 5, /gen command, command menu, update logging, telegraf v4 API fixes
This commit is contained in:
1 parent
c76676713f
commit
3ba969dbf4
2 files changed
+135
-50
No files matched your search
@@ -15,3 +15,5 @@ npm-debug.log*
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
bot.log
|
||||||
@@ -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)));
|
||||||
Reference in new issue
Block a user