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

Стек: Почему все оказалось проще, чем кажется
Для 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.
Архитектура: Проблема идентификации
Логика бота кажется тривиальной:
Клиент пишет -> Бот пересылает Админу.
Админ отвечает -> Бот пересылает Клиенту.
Сложность кроется во втором пункте. Когда Админ нажимает кнопку "Ответить" (Reply) в своем клиенте, бот видит входящее сообщение от Админа. Но как узнать, какому именно клиенту адресован ответ? В самом тексте сообщения ID клиента нет.
Нам нужно построить Карту сообщений (Message Map). Мы будем запоминать ID каждого сообщения, которое бот шлет Админу, и связывать его с ID Клиента.
Алгоритм:
Бот получает сообщение от Клиента.
Пересылает его Админу.
Запоминает в БД:
ID_этого_сообщения -> ID_клиента.Админ делает Reply на это сообщение.
Бот видит ID, на который ответили, находит в БД клиента и шлет ответ.

Пишем код
Подготовка
Создаем проект и устанавливаем зависимости. Нам понадобятся сама библиотека бота, драйвер 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

Настройка БД
Подключаем 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 бота обратной связи.

Запуск за 5 минут. Не нужны серверы, настройки портов и SSL-сертификаты. Скачал, вставил токен, работает.
Удобство для админа. Вы общаетесь с клиентами привычным способом — через кнопку "Ответить" (Reply), как в обычном чате. Бот сам разберется, кому отправить текст.
Надежность. Вся база контактов хранится в одном файле. Проще всего в мире бэкапить и переносить.
Код проекта доступен на GitHub: mikhail-klimenko/max-feedback-bot
Это только начало. Я буду рад, если вы присоединитесь к разработке — предлагайте пул-реквесты, заводите Issues с идеями или багами. Давайте сделаем инструмент для обратной связи в Max вместе!
В следующих частях добавим поддержку медиа и админку.
