В этой статье я расскажу, как создать бота для платформы MAX, который позволяет пользователям оставлять комментарии под постами в канале. Мы пройдём весь путь — от идеи до работающего решения, разберём все технические нюансы и ошибки, с которыми можно столкнуться.
Цель: автоматизировать сбор комментариев в отдельном чате, привязав их к конкретным постам, и сохранять всё в базу данных MySQL для дальнейшего использования (например, для сайта).
Архитектура решения
Наша система состоит из трёх основных компонентов:
- Канал MAX — публикуются посты.
- Бот — слушает канал, удаляет исходный пост, создаёт новый пост с кнопкой
open_appдля запуска мини-приложения. - Групповой чат — пользователи оставляют комментарии, которые бот сохраняет в базу данных.
- База данных MySQL — хранит посты и комментарии.
Схема работы:
- Пользователь публикует пост в канале.
- Бот получает событие, сохраняет пост в БД.
- Бот удаляет исходный пост.
- Бот отправляет новый пост с текстом оригинала и кнопкой
open_app. - Пользователь нажимает кнопку, открывается мини-приложение (или диплинк, если
open_appне работает). - Комментарии пишутся в чате, бот привязывает их к последнему посту.
Подготовка
1. Регистрация бота и мини-приложения
- Зайдите на платформу MAX для партнёров.
- Создайте бота и получите
BOT_TOKEN. - В настройках бота (раздел Расширенные настройки → Настроить) укажите URL вашего мини-приложения:
https://ваш-домен/max_app.html. - Выберите вид кнопки открытия (например, «Открыть») и сохраните.
Мини-приложение может быть простым HTML-файлом с формой для комментариев. Оно будет открываться внутри MAX при нажатии на кнопку open_app.
2. Создание чата для комментариев
Создайте отдельный групповой чат в MAX, куда будут приходить комментарии. Добавьте бота в этот чат как администратора.
3. Подготовка базы данных
Выполните SQL-скрипт для создания таблиц:
CREATE TABLE IF NOT EXISTS sukko_max_bot_channel_posts (
id INT AUTO_INCREMENT PRIMARY KEY,
post_id VARCHAR(255) NOT NULL UNIQUE,
chat_id VARCHAR(255) NOT NULL,
text TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS sukko_max_bot_comments (
id INT AUTO_INCREMENT PRIMARY KEY,
post_id VARCHAR(255) NOT NULL,
comment_text TEXT,
author_id VARCHAR(255),
author_name VARCHAR(255),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES sukko_max_bot_channel_posts(post_id) ON DELETE CASCADE
);
4. Настройка окружения
Создайте файл .env с переменными:
BOT_TOKEN=ваш_токен
CHANNEL_ID=-76112508207219 # ID канала
CHAT_ID=-68142474903667 # ID чата для комментариев
DB_HOST=localhost
DB_USER=пользователь
DB_PASSWORD=пароль
DB_NAME=sukkograd_db
DB_PREFIX=sukko_
Выбор языка и библиотеки
Первоначально мы разрабатывали бота на Python с использованием maxapi. Однако столкнулись с проблемами при работе с прямыми HTTP-запросами к новому API (platform-api2.max.ru) и установкой сертификатов Минцифры. После долгих попыток мы перешли на JavaScript (Node.js) с официальной библиотекой @maxhub/max-bot-api. Это оказалось более стабильным и документированным решением.
Установка зависимостей
npm init -y
npm install @maxhub/max-bot-api mysql2 dotenv axios
Создание бота
Основная логика
Бот состоит из двух обработчиков:
message_createdдля канала — реагирует на новые посты, сохраняет их, удаляет исходный пост и создаёт новый с кнопкойopen_app.message_createdдля чата — реагирует на сообщения в чате, сохраняет их как комментарии к последнему посту.
Ключевая сложность: поле webApp vs web_app
При попытке отправить кнопку open_app мы долго бились с ошибкой:
{"code":"proto.payload","message":"Field 'webApp' cannot be null"}
Мы перепробовали все варианты:
webApp: "https://..."(строка)webApp: { url: "..." }(объект)app_url: "..."(поле из неофициальной документации)url: "..."
Ничего не работало.
Решение пришло из ответа техподдержки: нужно использовать поле web_app (с подчёркиванием) и передавать имя бота (username), а не URL.
Для кнопкиopen_appиспользуйте полеweb_appсо значениемusernameвашего бота. Имя бота можно получить через запросGET /me.
Таким образом, правильный формат кнопки:
{
type: 'open_app',
text: 'Открыть приложение',
web_app: 'se13358334_bot' // имя бота
}
А URL мини-приложения указывается только в настройках бота на платформе.
Обработка постов в канале
bot.on('message_created', async (ctx) => {
const chatId = ctx.message?.recipient?.chat_id;
if (chatId !== CHANNEL_ID) return;
const text = ctx.message?.body?.text || '';
const msgId = ctx.message?.body?.mid;
if (!msgId) return;
// Сохраняем пост в БД
await savePost(msgId, chatId, text);
// Удаляем исходный пост
await ctx.deleteMessage(msgId);
// Получаем количество комментариев
const count = await getCommentsCount(msgId);
// Создаём кнопку open_app
const keyboard = {
type: 'inline_keyboard',
payload: {
buttons: [
[
{
type: 'open_app',
text: `💬 Комментарии (${count})`,
web_app: 'se13358334_bot' // имя бота
}
]
]
}
};
// Отправляем новый пост с кнопкой
await ctx.api.sendMessageToChat(CHANNEL_ID, text, { attachments: [keyboard] });
});
Обработка комментариев в чате
bot.on('message_created', async (ctx) => {
const chatId = ctx.message?.recipient?.chat_id;
if (chatId !== CHAT_ID) return;
if (ctx.user?.user_id === bot.botInfo.id) return;
const text = ctx.message?.body?.text || '';
if (text.startsWith('/')) return;
const authorId = ctx.user?.user_id || 'unknown';
const authorName = ctx.user?.username || ctx.user?.first_name || 'Аноним';
// Находим последний пост
const conn = await getDbConnection();
const [rows] = await conn.execute(
'SELECT post_id FROM sukko_max_bot_channel_posts ORDER BY created_at DESC LIMIT 1'
);
if (rows.length > 0) {
const postId = rows[0].post_id;
await saveComment(postId, text, authorId, authorName);
await ctx.reply('✅ Ваш комментарий сохранён!');
}
});
Полный код бота
Ниже приведён финальный рабочий код на Node.js.
const { Bot } = require('@maxhub/max-bot-api');
const mysql = require('mysql2/promise');
const dotenv = require('dotenv');
dotenv.config();
// Отключаем проверку SSL (для тестов, позже убрать)
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const BOT_TOKEN = process.env.BOT_TOKEN;
const CHANNEL_ID = parseInt(process.env.CHANNEL_ID);
const CHAT_ID = parseInt(process.env.CHAT_ID);
const dbConfig = {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
charset: 'utf8mb4'
};
const DB_PREFIX = process.env.DB_PREFIX || 'sukko_';
const TABLE_CHANNEL_POSTS = DB_PREFIX + 'max_bot_channel_posts';
const TABLE_COMMENTS = DB_PREFIX + 'max_bot_comments';
let pool;
async function getDbConnection() {
if (!pool) {
pool = mysql.createPool(dbConfig);
}
return pool;
}
async function savePost(postId, chatId, text) {
const conn = await getDbConnection();
try {
await conn.execute(
`INSERT INTO ${TABLE_CHANNEL_POSTS} (post_id, chat_id, text)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE text = VALUES(text), created_at = CURRENT_TIMESTAMP`,
[postId, String(chatId), text]
);
return true;
} catch (e) {
console.error(' ❌ Ошибка сохранения поста:', e.message);
return false;
}
}
async function getCommentsCount(postId) {
const conn = await getDbConnection();
try {
const [rows] = await conn.execute(
`SELECT COUNT(*) as count FROM ${TABLE_COMMENTS} WHERE post_id = ?`,
[postId]
);
return rows[0].count;
} catch (e) {
return 0;
}
}
async function saveComment(postId, text, authorId, authorName) {
const conn = await getDbConnection();
try {
await conn.execute(
`INSERT INTO ${TABLE_COMMENTS} (post_id, comment_text, author_id, author_name)
VALUES (?, ?, ?, ?)`,
[postId, text, String(authorId), authorName]
);
return true;
} catch (e) {
console.error(' ❌ Ошибка сохранения комментария:', e.message);
return false;
}
}
const bot = new Bot(BOT_TOKEN);
bot.command('start', (ctx) => ctx.reply('🤖 Бот для комментариев запущен!'));
// Обработчик постов в канале
bot.on('message_created', async (ctx) => {
const chatId = ctx.message?.recipient?.chat_id;
if (chatId !== CHANNEL_ID) return;
const text = ctx.message?.body?.text || '';
const msgId = ctx.message?.body?.mid;
if (!msgId) return;
console.log(`\n📢 ПОСТ В КАНАЛЕ`);
console.log(` ID: ${msgId}`);
console.log(` Текст: ${text.slice(0, 50)}...`);
await savePost(msgId, chatId, text);
console.log(' 💾 Пост сохранён в БД');
try {
await ctx.deleteMessage(msgId);
console.log(' 🗑️ Исходный пост удалён');
} catch (e) {
console.log(' ❌ Ошибка удаления:', e.message);
}
const count = await getCommentsCount(msgId);
// Отправляем новый пост с кнопкой open_app
try {
const keyboard = {
type: 'inline_keyboard',
payload: {
buttons: [
[
{
type: 'open_app',
text: `💬 Комментарии (${count})`,
web_app: 'se13358334_bot' // имя бота
}
]
]
}
};
await ctx.api.sendMessageToChat(CHANNEL_ID, text, { attachments: [keyboard] });
console.log(' ✅ Новый пост с кнопкой open_app отправлен!');
} catch (e) {
console.error(' ❌ Ошибка отправки open_app:', e.message);
// Fallback на диплинк (если open_app не работает)
try {
const deepLink = `https://max.ru/se13358334_bot?startapp=${msgId}`;
const fallbackKeyboard = {
type: 'inline_keyboard',
payload: {
buttons: [
[{ type: 'link', text: `💬 Комментарии (${count})`, url: deepLink }]
]
}
};
await ctx.api.sendMessageToChat(CHANNEL_ID, text, { attachments: [fallbackKeyboard] });
console.log(' ⚠️ Отправлен fallback-диплинк');
} catch (fallbackError) {
console.error(' ❌ Ошибка отправки fallback:', fallbackError.message);
}
}
});
// Обработчик комментариев в чате
bot.on('message_created', async (ctx) => {
const chatId = ctx.message?.recipient?.chat_id;
if (chatId !== CHAT_ID) return;
if (ctx.user?.user_id === bot.botInfo.id) return;
const text = ctx.message?.body?.text || '';
if (text.startsWith('/')) return;
const authorId = ctx.user?.user_id || 'unknown';
const authorName = ctx.user?.username || ctx.user?.first_name || 'Аноним';
console.log(`\n💬 НОВОЕ СООБЩЕНИЕ В ЧАТЕ`);
console.log(` Автор: ${authorName} (ID: ${authorId})`);
console.log(` Текст: ${text}`);
const conn = await getDbConnection();
try {
const [rows] = await conn.execute(
`SELECT post_id FROM ${TABLE_CHANNEL_POSTS} ORDER BY created_at DESC LIMIT 1`
);
if (rows.length > 0) {
const postId = rows[0].post_id;
await saveComment(postId, text, authorId, authorName);
console.log(` 🔗 Привязан к посту: ${postId}`);
await ctx.reply('✅ Ваш комментарий сохранён!');
} else {
console.log(' ⚠️ Нет постов для привязки');
}
} catch (e) {
console.error(' ❌ Ошибка:', e.message);
}
});
bot.start();
console.log('🚀 Бот запущен!');
console.log(`📢 Слушаем канал: ${CHANNEL_ID}`);
console.log(`💬 Слушаем чат: ${CHAT_ID}`);
Запуск и тестирование
- Сохраните код в файл
bot.js. - Установите зависимости:
npm install. - Создайте
.envс вашими данными. - Запустите бота:
node bot.js. - Опубликуйте тестовый пост в канале — бот должен обработать его, удалить и создать новый с кнопкой.
- Нажмите на кнопку — должно открыться мини-приложение.
- Напишите комментарий в чате — он сохранится в БД и привяжется к посту.
Выводы
- Правильное поле для кнопки
open_app—web_app(с подчёркиванием), значение — имя бота (username). - Библиотека
@maxhub/max-bot-apiстабильно работает с новым API MAX. - Всегда используйте fallback (диплинк) на случай, если
open_appне сработает. - Храните данные в структурированной БД для дальнейшего использования.
Эта архитектура гибкая и масштабируемая — вы можете легко добавить модерацию, уведомления или интеграцию с сайтом. Удачи в разработке!
🔗 Полезные ссылки на документацию MAX
-
Получение информации о боте (
GET /me) – чтобы узнатьusernameбота:
https://dev.max.ru/docs-api/methods/GET/me -
Подключение мини-приложения – настройка URL и кнопки на платформе:
https://dev.max.ru/docs/webapps/introduction -
Общее описание API и методы – для понимания структуры запросов:
https://dev.max.ru/docs-api -
Работа с клавиатурой и кнопками – описание типов кнопок (включая
open_app) в объектеChat:
https://dev.max.ru/docs-api/objects/Chat#inline-keyboard -
Библиотека
@maxhub/max-bot-apiна npm – официальная JS-библиотека для работы с MAX Bot API:
https://www.npmjs.com/package/@maxhub/max-bot-api