Много лет пользовался проектом https://github.com/antirek/alarmo для отправки сообщений с идентификацией по номеру телефона в viber и telegram. Но современные реалии диктуют новые условия и пришлось потратить время чтобы сделать аналогичное решение для нового мессенджера MAX. В текущем моменте регистрация ботов доступна только для корпоративного сегмента, но я думаю это не проблема.
Готового решения я не нашел, поэтому сделал на коленке свое, буду надеяться что кому-нибудь поможет и пригодится. Как поставить Docker и Docker-compose я думаю каждый найдет в интернете, а вот с новинкой в виде MAX пока информации маловато.
И так где-нибудь на сервере создайте папку max_bot или чтонибудь похожее, я создал в папке /home:
mkdir /home/max_bot touch docker-compose.yml touch Dockerfile touch .env touch index.js touch package.json
Скрытый текст
заполняем файлы
vi .env # Токен вашего бота, полученный от MAX BOT_TOKEN=trampampamtoken # Строка подключения к MongoDB (внутри Docker сети) MONGODB_URI=mongodb://mongo:27017 # Базы данных DB_NAME=maxuser DB_DELETE_NAME=max_deleteuser # Порт для HTTP-эндпоинта (внутри контейнера) PORT=4444
vi package.json { "name": "max-bot-docker", "version": "1.0.0", "description": "MAX Bot with MongoDB in Docker", "main": "index.js", "scripts": { "start": "node index.js" }, "dependencies": { "@maxhub/max-bot-api": "file:./libs/max-bot-api-client-ts", "dotenv": "^17.2.3", "express": "^5.1.0", "mongodb": "^4.17.1" }, "devDependencies": { "@types/express": "^5.0.5", "@types/node": "^20.10.0" }, "keywords": ["max", "bot", "docker", "mongodb"], "author": "sfefelov", "license": "MIT" }
vi Dockerfile FROM node:20-alpine AS builder # Установка git и инструментов для сборки RUN apk add --no-cache git python3 make g++ WORKDIR /usr/src/app # Клонируем и собираем библиотеку RUN git clone https://github.com/max-messenger/max-bot-api-client-ts.git ./libs/max-bot-api-client-ts WORKDIR /usr/src/app/libs/max-bot-api-client-ts RUN npm install RUN npm run build # <-- создаёт dist/ # Вернёмся в корень WORKDIR /usr/src/app # Копируем package.json и устанавливаем зависимости (включая локальную библиотеку) COPY package*.json ./ RUN npm install --omit=dev # экономим место, dev-зависимости не нужны в рантайме # Копируем исходники бота COPY . . EXPOSE 4444 CMD ["npm", "start"]
vi docker-compose.yml version: '3.8' services: mongo: image: mongo:4.0 restart: unless-stopped volumes: - mongo_data:/data/db ports: - "37017:27017" # <-- Внешний порт изменён на 37017, внутренний (27017) остался прежним # network_mode: service:bot # Опционально: если нужно, чтобы mongo и bot были в одной сети как один сервис bot: build: . restart: unless-stopped depends_on: - mongo environment: BOT_TOKEN: ${BOT_TOKEN} MONGODB_URI: mongodb://mongo:27017 # <-- Это НЕ МЕНЯЕТСЯ, так как указывает на имя сервиса 'mongo' и его внутренний порт 27017 DB_NAME: ${DB_NAME} DB_DELETE_NAME: ${DB_DELETE_NAME} PORT: ${PORT} ports: - "4444:4444" # Порт бота остается без изменений # volumes: # - "./src:/usr/src/app/node_modules/@maxhub/max-bot-api/dist" # - .:/usr/src/app dns: - 8.8.8.8 - 1.1.1.1 volumes: mongo_data: # Исправлено имя тома (убрана точка, точки не рекомендуются в именах томов)
vi index.js // index.js require('dotenv').config(); const { Bot } = require('@maxhub/max-bot-api'); const express = require('express'); const { MongoClient } = require('mongodb'); const { URLSearchParams } = require('url'); const app = express(); const PORT = process.env.PORT || 4444; const BOT_TOKEN = process.env.BOT_TOKEN; const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://mongo:27017'; const DB_NAME = process.env.DB_NAME || 'maxuser'; const DB_DELETE_NAME = process.env.DB_DELETE_NAME || 'max_deleteuser'; if (!BOT_TOKEN) { console.error('❌ BOT_TOKEN is required!'); process.exit(1); } const client = new MongoClient(MONGODB_URI); let db, deleteDb; async function connectDB() { await client.connect(); db = client.db(DB_NAME); deleteDb = client.db(DB_DELETE_NAME); console.log('✅ MongoDB connected'); } const bot = new Bot(BOT_TOKEN); bot.api.setMyCommands([ { name: 'start', description: 'Сказать привет' }, { name: 'Hi', description: 'Сказать привет' }, { name: 'hello', description: 'Сказать привет' }, { name: 'delete', description: 'Перенести профиль в архив' }, { name: 'bye', description: 'Перенести профиль в архив' }, ]); bot.command(['start', 'Hi', 'hello'], (ctx) => { ctx.reply( '👋 Привет! Я бот уведомлений компании **XXX**.\n' + 'По всем вопросам вы можете связаться: **XXX**.\n\n' + '📲 Пожалуйста, поделитесь своим номером телефона, чтобы получать уведомления:', { attachments: [ { type: 'inline_keyboard', payload: { buttons: [ [ { type: 'request_contact', text: '📱 Отправить номер' } ] ] } } ] } ); }); // ✅ Исправленный обработчик удаления bot.command(['delete', 'bye'], async (ctx) => { const userId = ctx.message.sender.user_id; const records = await db.collection('users').find({ userId }).toArray(); if (records.length === 0) { return ctx.reply('ℹ️ Вы не зарегистрированы.'); } // Переносим все записи await Promise.all( records.map(record => deleteDb.collection('deleted_users').insertOne({ ...record, movedAt: new Date(), reason: 'user_requested' }) ) ); // Удаляем из активной базы await db.collection('users').deleteMany({ userId }); ctx.reply('✅ Профиль перемещён в архив.'); }); function extractPhoneFromVCard(vcf) { const phoneMatch = vcf.match(/TEL[^:]*:(\+?\d+)/i); if (phoneMatch) { let raw = phoneMatch[1].replace(/\D/g, ''); if (raw.length === 11 && raw.startsWith('8')) return '7' + raw.slice(1); if (raw.length === 10) return '7' + raw; if (raw.length === 11 && raw.startsWith('7')) return raw; } return null; } bot.on('message_created', async (ctx) => { const msg = ctx.message; const userId = msg.sender.user_id; const chatId = msg.recipient.chat_id; if (msg.body?.attachments) { for (const att of msg.body.attachments) { if (att.type === 'contact' && att.payload?.vcf_info) { const phoneNumber = extractPhoneFromVCard(att.payload.vcf_info); if (!phoneNumber) { await ctx.reply('❌ Не удалось извлечь номер.'); return; } // 🔍 Проверка дубликата const existing = await db.collection('users').findOne({ phoneNumber }); if (existing) { await ctx.reply('ℹ️ Вы уже зарегистрированы с этим номером.'); return; } await db.collection('users').insertOne({ userId, chatId, phoneNumber, addedAt: new Date(), }); await ctx.reply(`✅ Номер **${phoneNumber}** добавлен в базу!`); return; } } } if (msg.body?.text) { await ctx.reply('Бот только отправляет сообщения, отвечать не умею'); return; } }); bot.catch((err) => { console.error('⚠️ Bot error:', err.message || err); }); // HTTP-отправка — рабочая версия app.get('/', async (req, res) => { const { to, text } = req.query; if (!to || !text) { return res.status(400).json({ error: 'Требуются параметры: to, text' }); } const digits = to.replace(/\D/g, ''); let phoneNumber = digits; if (digits.length === 11 && digits.startsWith('8')) { phoneNumber = '7' + digits.slice(1); } else if (digits.length === 10) { phoneNumber = '7' + digits; } if (phoneNumber.length !== 11 || !phoneNumber.startsWith('7')) { return res.status(400).json({ error: 'Некорректный номер телефона' }); } try { const users = await db.collection('users').find({ phoneNumber }).toArray(); if (users.length === 0) { return res.status(404).json({ error: 'Номер не найден среди активных пользователей' }); } const results = []; for (const user of users) { try { const url = new URL('https://platform-api.max.ru/messages'); url.searchParams.append('user_id', user.userId); const response = await fetch(url, { method: 'POST', headers: { 'Authorization': BOT_TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: user.chatId, text: text }) }); if (response.ok) { results.push({ userId: user.userId, chatId: user.chatId, status: 'sent' }); } else { const errText = await response.text(); results.push({ userId: user.userId, status: 'failed', reason: errText }); } } catch (err) { results.push({ userId: user.userId, status: 'failed', reason: err.message }); } } res.json({ success: true, sent_to: users.length, details: results, }); } catch (err) { console.error('💀 HTTP API error:', err); res.status(500).json({ error: 'Внутренняя ошибка сервера' }); } }); async function start() { await connectDB(); bot.start(); app.listen(PORT, '0.0.0.0', () => { console.log(`🚀 HTTP server on port ${PORT}`); }); } start().catch(console.error);
теперь надо осталось только вбить ТОКЕН в файл .env и запустить сборку находясь в папке max_bot выполнить:
docker-compose up --build
подождать пока все соберется и запустится.
После этого надо зайти в мессенджере MAX в своего бота, дать команду /start или /hello и поделиться номером телефона.
после чего со своего сервера вы сможете отправлять сообщения по номеру телефона например через:
curl http://<ip-srv>:4444/?to=79999999999&text=test
Как костяк проекта для миграции получилось на мой взгляд очень не плохо, особенности учтены только для моих задач, для таких же ленивых как я и кто мигрирует на отечественный мессенджер должно быть хорошо, быстро и приятно.
для еще более ленивых https://github.com/sfefelov/max_bot
