В этой статье я расскажу, как создать бота для платформы MAX, который позволяет пользователям оставлять комментарии под постами в канале. Мы пройдём весь путь — от идеи до работающего решения, разберём все технические нюансы и ошибки, с которыми можно столкнуться.

Цель: автоматизировать сбор комментариев в отдельном чате, привязав их к конкретным постам, и сохранять всё в базу данных MySQL для дальнейшего использования (например, для сайта).

Архитектура решения

Наша система состоит из трёх основных компонентов:

  1. Канал MAX — публикуются посты.
  2. Бот — слушает канал, удаляет исходный пост, создаёт новый пост с кнопкой open_app для запуска мини-приложения.
  3. Групповой чат — пользователи оставляют комментарии, которые бот сохраняет в базу данных.
  4. База данных 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

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

Основная логика

Бот состоит из двух обработчиков:

  1. message_created для канала — реагирует на новые посты, сохраняет их, удаляет исходный пост и создаёт новый с кнопкой open_app.
  2. 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}`);

Запуск и тестирование

  1. Сохраните код в файл bot.js.
  2. Установите зависимости: npm install.
  3. Создайте .env с вашими данными.
  4. Запустите бота: node bot.js.
  5. Опубликуйте тестовый пост в канале — бот должен обработать его, удалить и создать новый с кнопкой.
  6. Нажмите на кнопку — должно открыться мини-приложение.
  7. Напишите комментарий в чате — он сохранится в БД и привяжется к посту.

Выводы

  • Правильное поле для кнопки open_appweb_app (с подчёркиванием), значение — имя бота (username).
  • Библиотека @maxhub/max-bot-api стабильно работает с новым API MAX.
  • Всегда используйте fallback (диплинк) на случай, если open_app не сработает.
  • Храните данные в структурированной БД для дальнейшего использования.

Эта архитектура гибкая и масштабируемая — вы можете легко добавить модерацию, уведомления или интеграцию с сайтом. Удачи в разработке!

🔗 Полезные ссылки на документацию MAX