Много лет пользовался проектом 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
Как костяк проекта для миграции получилось на мой взгляд очень не плохо, особенности учтены только для моих задач, для таких же ленивых как я и кто мигрирует на отечественный мессенджер должно быть хорошо, быстро и приятно.