29 мая 2024

🤖👨‍💻 Пишем Telegram-бота для подготовки к собеседованию на Frontend-разработчика

🔥 Алексей. Занимаюсь frontend-разработкой и пишу SQL скрипты в одной it компании. Веду личный блог о моем опыте в IT и около IT на YouTube - https://www.youtube.com/@tehno.maniak
Представляю бесплатного Telegram-бота, разработанного для эффективной подготовки к техническому собеседованию на позицию Frontend разработчика. Бот предлагает викторины по HTML, CSS, JavaScript и React, а также рейтинговый режим для соревнования с другими пользователями. Полный код проекта можно посмотреть в моем Github-репозитории.
4
🤖👨‍💻 Пишем Telegram-бота для подготовки к собеседованию на Frontend-разработчика

Описание работы Telegram-бота

В боте присутствует 4 категории вопросов: HTML, CSS, JavaScript и React.

Выбор категории вопросов в Telegram-боте для подготовки к собеседованию
Выбор категории вопросов в Telegram-боте для подготовки к собеседованию

Чтобы было интереснее решать вопросы, я добавил еще 🏆Рейтинговый режим, в котором отсутствует выбор категории и вам предстоит решать все имеющиеся в базе вопросы. За каждый правильный ответ вам присуждается 1 балл. В случае неправильного ответа игра прекратится, а ее результат будет записан в базу данных и сохранен в вашем профиле.

Для добавления элемента соревнования между другими пользователями бота я добавил кнопку – Таблица лидеров, после нажатия на которую будет выведено ТОП-10 игроков Рейтингового режима.

У каждого игрока есть свой 👨🏼‍💻Профиль, в котором можно увидеть статистику по решенным вопросам в 4 категориях и баллы за игру в 🏆Рейтинговом режиме.

Личный профиль каждого пользователя
Личный профиль каждого пользователя
👨‍💻🎨 Библиотека фронтендера
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека фронтендера»

Используемые технологии

  1. Node.js: Серверная платформа для выполнения JavaScript-кода.
  2. grammY: Фреймворк для создания Telegram-ботов.
  3. sqlite: Встраиваемая база данных для хранения результатов пользователей.
  4. date-fns: Библиотека для форматирования дат и времени.
  5. dotenv: Модуль для загрузки переменных окружения из .env файла.
package.json
         "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

    
  1. index.js: Основной файл проекта, содержащий весь код логики бота, включая инициализацию, обработку команд и взаимодействие с базой данных SQLite.
  2. questions/: Директория, содержащая JSON-файлы с вопросами по различным категориям (HTML, CSS, JavaScript, React).

Пример файла html_questions.json:

html_questions.json
        {
  "questions": [
    {
      "question": "Какой тег используется для создания ссылки?",
      "options": ["<link>", "<a>", "<div>", "<img>"],
      "correctOption": 1
    },
    {
      "question": "Какой тег используется для создания списка?",
      "options": ["<list>", "<ul>", "<ol>", "<menu>"],
      "correctOption": 2
    },
  ]
}

    
  1. leaderboard.db: Файл базы данных SQLite, в котором хранятся данные о пользователях, их результаты и время последней игры.
  2. .env: Этот файл содержит конфиденциальные данные, такие как токен API Telegram и ID администратора.

Пример файла .env:

Пример файла .env
        BOT_API_KEY=your-telegram-bot-api-key
ADMIN_ID=your-telegram-id

    

Создание Telegram-бота

Создадим экземпляр бота и инициализируем его с API-ключом Telegram:

index.js
        const bot = new Bot(process.env.BOT_API_KEY);
    

Определим начальное состояние сессии для каждого пользователя, которое взаимодействует с ботом:

index.js
        bot.use(session({
  initial: () => ({
    correctAnswers: {
      html: 0,
      css: 0,
      js: 0,
      react: 0
    },
    hasStartedRatingMode: false
  })
}));
    

Для скорости работы Telegram-бота добавим функцию для загрузки вопросов из JSON-файлов при запуске бота:

index.js
        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, если она еще не существует:

index.js
        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.
index.js
        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. Эта функция обновляет таблицу лидеров для пользователя, добавляя или обновляя его запись в зависимости от результатов.
index.js
        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 пользователей из таблицы лидеров, отсортированных по убыванию очков.
index.js
        async function getLeaderboard() {
  return await db.all('SELECT username, score FROM leaderboard ORDER BY score DESC LIMIT 10');
}
    
  • Функция getTotalUsers. Эта функция возвращает общее количество пользователей в таблице лидеров.
index.js
        async function getTotalUsers() {
  const result = await db.get('SELECT COUNT(*) AS count FROM leaderboard');
  return result.count;
}
    

Добавим функции обработки для команд Telegram-бота /start, /profile, /admin.

  • Команда /start: Эта команда инициализирует бота и приветствует пользователя, предлагая начать использование бота.
index.js
        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: Эта команда отображает профиль пользователя, включая его результаты в различных категориях.
index.js
        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 администратора. Она отображает общее количество пользователей.
index.js
        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('У вас нет прав для использования этой команды.');
  }
});
    

Осталось добавить функционал бота, связанный с обработкой сообщений пользователей и викторинами.

  • Основная функция обработки сообщений. В зависимости от текста сообщения, бот вызывает соответствующую функцию для начала викторины по выбранной категории, запуска рейтингового режима или отображения таблицы лидеров.
index.js
        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. Эта функция обрабатывает ответы пользователя на вопросы викторины.
index.js
        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. Эта функция начинает викторину по указанной категории.
index.js
        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;
  }
    
💼 Вакансии для фронтенд-разработчиков
Вакансии по фронтенду, джаваскрипт, React, Angular, Vue @jsdevjob

Заключение

В этой статье я рассмотрел основные составляющие моего Telegram-бота, необходимые для его работы. Полный код проекта можно посмотреть в моем Github репозитории. Опробовать бота можно по ссылке. У меня есть еще пара идей для улучшения его функциональности в будущем, планирую добавить блок вопросов с задачами по JavaScript и добавить больше статистик в профилях пользователей.

Telegram бот для подготовки к собеседованию на Frontend разработчика
Telegram бот для подготовки к собеседованию на Frontend разработчика

Комментарии

 
 

ВАКАНСИИ

Добавить вакансию
Ведущий SRE инженер
Москва, по итогам собеседования
Senior MLE (SE)
от 5000 USD до 9000 USD
Senior DevOps Developer
Лимасол, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

LIVE >

Подпишись

на push-уведомления