Привет, Хабр! С выходом платформы MAX у разработчиков появилось новое игровое поле. Пока комьюнити спорит о шансах на победу в гонке мессенджеров, маркетологи уже начали переливать туда трафик.

Самая типовая задача для бизнеса сейчас — бот обратной связи. В Telegram эту нишу давно занял Olgram, а вот в Max — чистый лист. Давайте вместе напишем свой аналог. Это отличный кейс, чтобы разобраться с новым API, не углубляясь в лишнюю инфраструктуру.

Кнопка MAX на сайте — "точка входа" для клиентов.
Кнопка MAX на сайте — "точка входа" для клиентов.

Стек: Почему все оказалось проще, чем кажется

Для MVP (Minimum Viable Product) мы будем использовать Node.js и официальную библиотеку @maxhub/max-bot-api.

Здесь важно отметить один момент, который сэкономит вам кучу времени. В документации Max упор делается на Webhooks. Это значит, что по классике вам нужен сервер с "белым" IP, HTTPS-сертификат и, скорее всего, танцы с ngrok или Cloudflare Tunnel для локальной разработки.

Но официальная JS-библиотека поддерживает Long Polling "из коробки". Бот сам опрашивает сервера Max. Это позволяет запустить проект на локалке без единого открытого порта. Никаких ngrok, никаких настроек серверов. Просто npm start и работает.

База данных: SQLite

Второй камень преткновения — выбор БД. Разработчики частенько тянут в простые проекты PostgreSQL или MySQL. Для пет-проекта или MVP это overkill. Мы берем SQLite.

  • Это просто файл в папке. Нет отдельного процесса сервера.

  • Нет сетевых настроек и задержек (база лежит рядом с кодом).

  • Идеально подходит для чат-ботов, где запросы идут последовательно.

Если ваш бот вдруг станет популярным и упрется в конкуренцию записи — тогда вы переедете на Postgres. А пока не тратим время на DevOps.

Архитектура: Проблема идентификации

Логика бота кажется тривиальной:

  1. Клиент пишет -> Бот пересылает Админу.

  2. Админ отвечает -> Бот пересылает Клиенту.

Сложность кроется во втором пункте. Когда Админ нажимает кнопку "Ответить" (Reply) в своем клиенте, бот видит входящее сообщение от Админа. Но как узнать, какому именно клиенту адресован ответ? В самом тексте сообщения ID клиента нет.

Нам нужно построить Карту сообщений (Message Map). Мы будем запоминать ID каждого сообщения, которое бот шлет Админу, и связывать его с ID Клиента.

Алгоритм:

  1. Бот получает сообщение от Клиента.

  2. Пересылает его Админу.

  3. Запоминает в БД: ID_этого_сообщения -> ID_клиента.

  4. Админ делает Reply на это сообщение.

  5. Бот видит ID, на который ответили, находит в БД клиента и шлет ответ.

Администратор отвечает через Reply. Бот подтверждает отправку.
Администратор отвечает через Reply. Бот подтверждает отправку.

Пишем код

Подготовка

Создаем проект и устанавливаем зависимости. Нам понадобятся сама библиотека бота, драйвер SQLite и пакет для переменных окружения.

npm init -y
npm install @maxhub/max-bot-api sqlite sqlite3 dotenv

Создаем файл .env. Токен бота берем в интеграциях на business.max.ru, а OWNER_ID — это ваш личный ID в Max (его можно узнать в логах при первом запуске).

BOT_TOKEN=ваш_токен_здесь
OWNER_ID=12345678
Получение токена для ботов в MAX
Получение токена для ботов в MAX

Настройка БД

Подключаем SQLite и создаем таблицу для маппинга. Обратите внимание на использование async/await — библиотека sqlite отлично с ним дружит.

import { open } from 'sqlite';
import sqlite3 from 'sqlite3';

let db;
(async () => {
  db = await open({ filename: './database.sqlite', driver: sqlite3.Database });
  
  // Таблица для связи ID сообщения -> ID клиента
  await db.exec(`
    CREATE TABLE IF NOT EXISTS reply_map (
      owner_msg_mid TEXT PRIMARY KEY,
      client_user_id INTEGER
    )
  `);
  console.log('База данных готова');
})();

Входящий поток (Клиент -> Админ)

Главный нюанс: метод sendMessageToUser возвращает объект отправленного сообщения. Нам нужно достать из него mid (Message ID), чтобы сохранить в базу. Без этого мы не сможем "привязать" ответ админа к конкретному пользователю.

const ownerId = Number(process.env.OWNER_ID); // Ваш ID

bot.on('message_created', async (ctx) => {
  const msg = ctx.message;
  const senderId = msg.sender.user_id;
  const text = msg.body.text;

  // Если пишет КЛИЕНТ (не админ)
  if (senderId !== ownerId) {
    
    const forwardText = `📩 **Сообщение от ${msg.sender.first_name}** (ID: ${senderId}):\n\n${text}`;
    
    // Отправляем админу с Markdown-разметкой
    const sentMsg = await bot.api.sendMessageToUser(ownerId, forwardText, { format: 'markdown' });

    // ГЛАВНЫЙ МОМЕНТ: Сохраняем связь
    if (sentMsg && sentMsg.body && sentMsg.body.mid) {
        await db.run('INSERT OR REPLACE INTO reply_map (owner_msg_mid, client_user_id) VALUES (?, ?)', 
          [sentMsg.body.mid, senderId]);
    }
    return;
  }

  // ... здесь будет логика ответов
});

Исходящий поток (Админ -> Клиент)

Теперь самое интересное. Как понять, что Админ нажал кнопку "Ответить"? Изучая объект сообщения (msg), который присылает API, можно заметить поле link. Оно находится в корне объекта, на уровне с body и sender.

Структура JSON при Reply выглядит так:

{
  "body": { "text": "ответ", ... },
  "sender": { ... },
  "link": {              // <-- Идентификатор ответа
    "type": "reply",
    "message": {
      "mid": "mid.исходного_сообщения..." // ID сообщения, на которое ответили
    }
  }
}

Реализуем поиск получателя. Если админ просто пишет текст (не Reply), отправляем последнему активному клиенту (fallback).

// Продолжение внутри bot.on...

  // Если пишет ВЛАДЕЛЕЦ
  if (senderId === ownerId) {
    
    // Проверяем, что это именно Reply
    if (msg.link && msg.link.type === 'reply') {
      
      // Достаем ID сообщения, на которое ответили
      const repliedMsgMid = msg.link.message.mid;
      
      // Ищем в нашей базе клиента
      const target = await db.get('SELECT client_user_id FROM reply_map WHERE owner_msg_mid = ?', repliedMsgMid);

      if (target) {
        await bot.api.sendMessageToUser(target.client_user_id, text);
        await ctx.reply(`✅ Ответ отправлен пользователю ID: ${target.client_user_id}`);
      } else {
        await ctx.reply('⚠️ Пользователь не найден в базе (возможно, старое сообщение).');
      }
      return;
    }
    
    // Fallback: Если Админ просто пишет текст, отправляем последнему активному
    // (Для простоты MVP сохраняем lastClient в глобальной переменной)
  }

Итог

У нас на руках рабочий MVP бота обратной связи.

Пример блока для связи на сайте. Кнопка MAX позволяет клиентам писать напрямую, минуя лишние формы.
Пример блока для связи на сайте. Кнопка MAX позволяет клиентам писать напрямую, минуя лишние формы.
  • Запуск за 5 минут. Не нужны серверы, настройки портов и SSL-сертификаты. Скачал, вставил токен, работает.

  • Удобство для админа. Вы общаетесь с клиентами привычным способом — через кнопку "Ответить" (Reply), как в обычном чате. Бот сам разберется, кому отправить текст.

  • Надежность. Вся база контактов хранится в одном файле. Проще всего в мире бэкапить и переносить.

Код проекта доступен на GitHub: mikhail-klimenko/max-feedback-bot

Это только начало. Я буду рад, если вы присоединитесь к разработке — предлагайте пул-реквесты, заводите Issues с идеями или багами. Давайте сделаем инструмент для обратной связи в Max вместе!

В следующих частях добавим поддержку медиа и админку.