🤖👨💻 Пишем Telegram-бота для подготовки к собеседованию на Frontend-разработчика
Представляю бесплатного Telegram-бота, разработанного для эффективной подготовки к техническому собеседованию на позицию Frontend разработчика. Бот предлагает викторины по HTML, CSS, JavaScript и React, а также рейтинговый режим для соревнования с другими пользователями. Полный код проекта можно посмотреть в моем Github-репозитории.
Описание работы Telegram-бота
В боте присутствует 4 категории вопросов: HTML, CSS, JavaScript и React.
Чтобы было интереснее решать вопросы, я добавил еще 🏆Рейтинговый режим, в котором отсутствует выбор категории и вам предстоит решать все имеющиеся в базе вопросы. За каждый правильный ответ вам присуждается 1 балл. В случае неправильного ответа игра прекратится, а ее результат будет записан в базу данных и сохранен в вашем профиле.
Для добавления элемента соревнования между другими пользователями бота я добавил кнопку – Таблица лидеров, после нажатия на которую будет выведено ТОП-10 игроков Рейтингового режима.
У каждого игрока есть свой 👨🏼💻Профиль, в котором можно увидеть статистику по решенным вопросам в 4 категориях и баллы за игру в 🏆Рейтинговом режиме.
Используемые технологии
- Node.js: Серверная платформа для выполнения JavaScript-кода.
- grammY: Фреймворк для создания Telegram-ботов.
- sqlite: Встраиваемая база данных для хранения результатов пользователей.
- date-fns: Библиотека для форматирования дат и времени.
- dotenv: Модуль для загрузки переменных окружения из
.env
файла.
"dependencies": { "date-fns": "^3.6.0", "dotenv": "^16.4.5", "grammy": "^1.23.0", "nodemon": "^3.1.0", "sqlite": "^5.1.1", "sqlite3": "^5.1.7" }
Структура проекта
Рассмотрим структуру проекта, которая включает все необходимые файлы и директории.
tech-interview-trainer/ ├── .env ├── index.js ├── package.json ├── package-lock.json ├── README.md └── questions/ ├── html_questions.json ├── css_questions.json ├── js_questions.json └── react_questions.json └── leaderboard.db
- index.js: Основной файл проекта, содержащий весь код логики бота, включая инициализацию, обработку команд и взаимодействие с базой данных SQLite.
- questions/: Директория, содержащая JSON-файлы с вопросами по различным категориям (HTML, CSS, JavaScript, React).
Пример файла html_questions.json
:
{ "questions": [ { "question": "Какой тег используется для создания ссылки?", "options": ["<link>", "<a>", "<div>", "<img>"], "correctOption": 1 }, { "question": "Какой тег используется для создания списка?", "options": ["<list>", "<ul>", "<ol>", "<menu>"], "correctOption": 2 }, ] }
- leaderboard.db: Файл базы данных SQLite, в котором хранятся данные о пользователях, их результаты и время последней игры.
- .env: Этот файл содержит конфиденциальные данные, такие как токен API Telegram и ID администратора.
Пример файла .env:
BOT_API_KEY=your-telegram-bot-api-key ADMIN_ID=your-telegram-id
Создание Telegram-бота
Создадим экземпляр бота и инициализируем его с API-ключом Telegram:
const bot = new Bot(process.env.BOT_API_KEY);
Определим начальное состояние сессии для каждого пользователя, которое взаимодействует с ботом:
bot.use(session({ initial: () => ({ correctAnswers: { html: 0, css: 0, js: 0, react: 0 }, hasStartedRatingMode: false }) }));
Для скорости работы Telegram-бота добавим функцию для загрузки вопросов из JSON-файлов при запуске бота:
async function loadQuestions() { const categories = { html: 'html_questions.json', css: 'css_questions.json', js: 'js_questions.json', react: 'react_questions.json' }; for (const [category, file] of Object.entries(categories)) { try { const data = await fs.readFile(`questions/${file}`, 'utf8'); questionsData[category] = JSON.parse(data).questions; } catch (error) { console.error(`Ошибка при загрузке вопросов из файла ${file}:`, error); } } }
Для хранения данных о пользователях и их результатах в таблице лидеров инициализируем базу данных SQLite. Эта функция открывает (или создает) базу данных и создает таблицу leaderboard, если она еще не существует:
async function initDatabase() { db = await open({ filename: 'leaderboard.db', driver: sqlite3.Database }); await db.exec(` CREATE TABLE IF NOT EXISTS leaderboard ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, score INTEGER NOT NULL, last_played TEXT NOT NULL ) `); }
Добавим функции для работы с базой данных SQLite для управления профилями пользователей и их результатами в таблице лидеров.
- Функция createProfile. Эта функция создает профиль для пользователя, если его еще нет в таблице leaderboard.
async function createProfile(username) { const existingEntry = await db.get('SELECT * FROM leaderboard WHERE username = ?', username); if (!existingEntry) { await db.run('INSERT INTO leaderboard (username, score, last_played) VALUES (?, ?, ?)', username, 0, 'Еще не играл'); } }
- Функция updateLeaderboard. Эта функция обновляет таблицу лидеров для пользователя, добавляя или обновляя его запись в зависимости от результатов.
async function updateLeaderboard(username, score) { const now = new Date().toISOString(); const existingEntry = await db.get('SELECT * FROM leaderboard WHERE username = ?', username); if (existingEntry) { if (existingEntry.score < score) { await db.run('UPDATE leaderboard SET score = ?, last_played = ? WHERE username = ?', score, now, username); } else { await db.run('UPDATE leaderboard SET last_played = ? WHERE username = ?', now, username); } } else { await db.run('INSERT INTO leaderboard (username, score, last_played) VALUES (?, ?, ?)', username, score, now); } }
- Функция getLeaderboard. Эта функция возвращает топ 10 пользователей из таблицы лидеров, отсортированных по убыванию очков.
async function getLeaderboard() { return await db.all('SELECT username, score FROM leaderboard ORDER BY score DESC LIMIT 10'); }
- Функция getTotalUsers. Эта функция возвращает общее количество пользователей в таблице лидеров.
async function getTotalUsers() { const result = await db.get('SELECT COUNT(*) AS count FROM leaderboard'); return result.count; }
Добавим функции обработки для команд Telegram-бота /start, /profile, /admin.
- Команда /start: Эта команда инициализирует бота и приветствует пользователя, предлагая начать использование бота.
bot.command('start', async (ctx) => { //Получение имени пользователя: const username = ctx.from.username || ctx.from.first_name; //Вызывается функция createProfile, чтобы создать профиль пользователя, если он еще не существует. await createProfile(username); //Создается клавиатура с кнопками для выбора темы. const startKeyboard = getStartKeyboard(); //Отправляется приветственное сообщение и клавиатура с кнопками. await ctx.reply( 'Привет! Я помогу тебе подготовиться к собеседованию. Используй команды ниже для взаимодействия с ботом:\n' + '/start - Начать использование бота\n' + '/profile - Просмотр вашего профиля', { reply_markup: startKeyboard } ); await ctx.reply('С чего начнем? Выбирай тему👇', { reply_markup: startKeyboard, }); });
- Команда /profile: Эта команда отображает профиль пользователя, включая его результаты в различных категориях.
bot.command('profile', async (ctx) => { //Получение имени пользователя: const username = ctx.from.username || ctx.from.first_name; //Получение данных пользователя из базы данных: const result = await db.get('SELECT * FROM leaderboard WHERE username = ?', username); //Получение общего количества вопросов по категориям: const htmlQuestionsTotal = questionsData.html.length; const cssQuestionsTotal = questionsData.css.length; const jsQuestionsTotal = questionsData.js.length; const reactQuestionsTotal = questionsData.react.length; //Получение количества правильных ответов пользователя из сессии: const htmlCorrect = ctx.session.correctAnswers.html; const cssCorrect = ctx.session.correctAnswers.css; const jsCorrect = ctx.session.correctAnswers.js; const reactCorrect = ctx.session.correctAnswers.react; if (result) { const formattedDate = result.last_played === 'Еще не играл' ? result.last_played : format(new Date(result.last_played), 'dd MMMM yyyy, HH:mm', { locale: ru }); const profileMessage = `👤 Профиль пользователя ${username}:\n` + `🏆 Счет в рейтинговой игре: ${result.score} очков\n` + `📅 Дата последней игры: ${formattedDate}\n` + `📚 Вопросы по HTML: решено верно ${htmlCorrect} из ${htmlQuestionsTotal}\n` + `📚 Вопросы по CSS: решено верно ${cssCorrect} из ${cssQuestionsTotal}\n` + `📚 Вопросы по JavaScript: решено верно ${jsCorrect} из ${jsQuestionsTotal}\n` + `📚 Вопросы по React: решено верно ${reactCorrect} из ${reactQuestionsTotal}`; await ctx.reply(profileMessage); } else { await ctx.reply('Профиль не найден. Начните игру в рейтинговом режиме, чтобы создать профиль.'); } });
- Команда /admin: Эта команда предназначена для администрирования и доступна только пользователю с ID администратора. Она отображает общее количество пользователей.
bot.command('admin', async (ctx) => { const userId = ctx.from.id; const adminId = parseInt(process.env.ADMIN_ID, 10); if (userId === adminId) { const totalUsers = await getTotalUsers(); await ctx.reply(`Общее количество пользователей: ${totalUsers}`); } else { await ctx.reply('У вас нет прав для использования этой команды.'); } });
Осталось добавить функционал бота, связанный с обработкой сообщений пользователей и викторинами.
- Основная функция обработки сообщений. В зависимости от текста сообщения, бот вызывает соответствующую функцию для начала викторины по выбранной категории, запуска рейтингового режима или отображения таблицы лидеров.
bot.on('message', async (ctx) => { const { text } = ctx.message; if (text === 'Назад ↩️') { const startKeyboard = getStartKeyboard(); await ctx.reply('Выберите категорию:', { reply_markup: startKeyboard, }); } else { switch (text) { case 'HTML': await startQuiz(ctx, 'html'); break; case 'CSS': await startQuiz(ctx, 'css'); break; case 'JavaScript': await startQuiz(ctx, 'js'); break; case 'React': await startQuiz(ctx, 'react'); break; case '🏆Рейтинговый режим': if (!ctx.session.hasStartedRatingMode) { ctx.session.hasStartedRatingMode = true; await ctx.reply( 'Рейтинговый режим содержит вопросы из всех категорий. За каждый правильный ответ дается балл, а при неверном ответе игра прекращается. Таблица лидеров выводит топ 10 игроков в рейтинге.' ); } initializeRatingMode(ctx); await startRatingQuiz(ctx); break; case '📣Таблица лидеров': await showLeaderboard(ctx); break; default: handleQuizAnswer(ctx, text); } } });
- Функция handleQuizAnswer. Эта функция обрабатывает ответы пользователя на вопросы викторины.
async function handleQuizAnswer(ctx, answer) { try { if (!ctx.session.currentQuestion) { await ctx.reply('Кажется, я забыл вопрос. Давай начнем заново.'); return; } const correctAnswer = ctx.session.currentQuestion.options[ctx.session.currentQuestion.correctOption]; if (answer === correctAnswer) { await ctx.reply('Верно!'); ctx.session.correctAnswers[ctx.session.currentCategory]++; if (ctx.session.ratingMode) { ctx.session.score += 1; await startRatingQuiz(ctx); } else { await startQuiz(ctx, ctx.session.currentCategory); } } else { if (ctx.session.ratingMode) { const username = ctx.from.username || ctx.from.first_name; await updateLeaderboard(username, ctx.session.score); ctx.session.ratingMode = false; const startKeyboard = getStartKeyboard(); await ctx.reply(`Ошибка! Вы набрали ${ctx.session.score} очков.`, { reply_markup: startKeyboard, }); ctx.session.score = 0; } else { await ctx.reply('Неправильно. Попробуйте еще раз.'); } } } catch (error) { console.error('Ошибка обработки ответа на вопрос:', error); await ctx.reply('Произошла ошибка при обработке ответа на вопрос. Попробуйте еще раз позже.'); } }
- Функция startQuiz. Эта функция начинает викторину по указанной категории.
async function startQuiz(ctx, category) { initializeQuizState(ctx, category); const questions = questionsData[category]; if (!questions) { await ctx.reply(`Не удалось загрузить вопросы для категории ${category.toUpperCase()}. Проверьте файл: questions/${category}_questions.json`); return; } const questionData = getRandomQuestion(questions, ctx.session.askedQuestions[category]); if (!questionData) { const startKeyboard = getStartKeyboard(); await ctx.reply(`Вы ответили на все вопросы по ${category.toUpperCase()}!`, { reply_markup: startKeyboard, }); return; }
Заключение
В этой статье я рассмотрел основные составляющие моего Telegram-бота, необходимые для его работы. Полный код проекта можно посмотреть в моем Github репозитории. Опробовать бота можно по ссылке. У меня есть еще пара идей для улучшения его функциональности в будущем, планирую добавить блок вопросов с задачами по JavaScript и добавить больше статистик в профилях пользователей.