apigram
v1.5.0
Published
Multi-user Telegram API gateway (MTProto) — REST + WebSocket
Maintainers
Readme
apiGram
Multi-user Telegram API gateway (MTProto) — REST, WebSocket и MCP-сервер для AI-агентов.
Один процесс держит пул TelegramClient по одному на аккаунт; любое клиентское приложение
подключается по HTTP и получает realtime-поток по WebSocket — а AI-агент управляет тем же
аккаунтом через MCP-инструменты, без отдельного слоя интеграции.
English version: README.md
Содержание
- Установка
- Скрипт
run.sh - Переменные окружения
- Быстрый старт
- Эндпоинты
- WebSocket
- MCP — подключение AI-агентов
- Ошибки
- Браузерные клиенты
- Прокси
- Безопасность
- Ограничения
- Тесты
- Changelog
Установка
npm install apigramЛибо склонировать и запустить из исходников:
git clone https://github.com/emaxe/apiGram.git
cd apiGram
npm install
cp .env.example .env # задайте TELEGRAM_API_ID / TELEGRAM_API_HASH
npm startКлючи API — на https://my.telegram.org → API development tools.
При установке пакетом шлюз доступен и как бинарник:
npx apigramТребуется Node.js >= 18.
Скрипт run.sh
В репозитории есть интерактивный помощник, покрывающий все режимы:
./run.sh # меню
./run.sh <команда> # прямой вызов, напр. ./run.sh dev| Команда | Действие |
|---|---|
| install | npm ci (по lock-файлу) или npm install |
| start | запуск сервера с проверкой .env, зависимостей и занятости порта |
| start:proxy | запуск сервера через прокси (кэширование настроек в data/.proxy) |
| dev | запуск с автоперезапуском (node --watch src/index.js) |
| dev:proxy | запуск с автоперезапуском через прокси |
| test | юнит-тесты |
| smoke | сквозная проверка на живом аккаунте (scripts/smoke.mjs) |
| proxy | настройка, просмотр или сброс кэшированного прокси |
| env | показать конфигурацию с маскированием секретов, создать .env из примера |
| health | GET /v1/health по адресу из .env |
| doctor | диагностика: версия Node, зависимости, ключи, каталог данных, порт |
| clean | удаление node_modules, лога обновлений или кэша прокси |
Адрес для start/health/smoke берётся из .env, а не задан жёстко. clean не трогает
data/accounts.json — там сессии Telegram.
Переменные окружения
| Переменная | По умолчанию | Назначение |
|---|---|---|
| TELEGRAM_API_ID / TELEGRAM_API_HASH | — | обязательны |
| HOST / PORT | 127.0.0.1 / 3111 | адрес прослушивания |
| ADMIN_TOKEN | пусто | требуется для POST /v1/accounts; пусто = эндпоинт открыт (только для localhost) |
| DATA_DIR | ./data | реестр аккаунтов и лог обновлений (режим 0600) |
| LOG_UPDATES | false | писать поток обновлений в data/updates.jsonl |
| UPDATES_MAX_MB | 50 | порог ротации лога |
| LOG_MEDIA_TIMING | false | замер выдачи файла в журнал: описание, первый байт, скорость |
| CORS_ORIGINS | пусто | источники, которым разрешены браузерные запросы (через запятую); пусто = CORS выключен |
| PROXY_URL | пусто | прокси для MTProto: socks5://, socks4://, http://, https://, mtproxy://; пусто = прямое подключение |
| PROXY_TIMEOUT | 5 | таймаут подключения к прокси, секунды |
| PROXY_FROM_ENV | false | при пустом PROXY_URL брать прокси из https_proxy → all_proxy → http_proxy (регистр любой) |
| AUTOCONNECT_ACCOUNTS | false | подключать все аккаунты с сохранённой сессией при старте процесса |
Быстрый старт
BASE=http://127.0.0.1:3111/v1
# 1. Создать аккаунт — apiToken показывается один раз
curl -X POST $BASE/accounts -H 'content-type: application/json' -d '{"name":"my"}'
# -> { "accountId": "acc_…", "apiToken": "tok_…", "status": "no_session" }
ACC=acc_…; TOKEN=tok_…
AUTH="Authorization: Bearer $TOKEN"
# 2. Логин: телефон → код → (2FA, если включён)
curl -X POST $BASE/accounts/$ACC/auth/send-code -H "$AUTH" -H 'content-type: application/json' -d '{"phone":"+79991234567"}'
curl -X POST $BASE/accounts/$ACC/auth/verify-code -H "$AUTH" -H 'content-type: application/json' -d '{"code":"12345"}'
# -> { "next": "done", "me": {…} } либо { "next": "password" }
curl -X POST $BASE/accounts/$ACC/auth/password -H "$AUTH" -H 'content-type: application/json' -d '{"password":"…"}'
# 3. Отправить сообщение
curl -X POST $BASE/accounts/$ACC/chat/@username/messages -H "$AUTH" -H 'content-type: application/json' -d '{"text":"hello"}'
# 4. Отправить файлы (до 10 за раз, поле формы — files)
curl -X POST $BASE/accounts/$ACC/chat/@username/files -H "$AUTH" -F [email protected] -F caption=ПриветЭндпоинты
Все, кроме POST /v1/accounts и GET /v1/health, требуют Authorization: Bearer <apiToken>.
GET /v1/health проверка живости
# Аккаунты и авторизация
POST /v1/accounts создать аккаунт (ADMIN_TOKEN, если задан)
GET /v1/accounts свои аккаунты
DELETE /v1/accounts/:id удалить аккаунт
POST /v1/accounts/:id/auth/send-code { phone } → код
POST /v1/accounts/:id/auth/verify-code { code } → { next: "done"|"password" }
POST /v1/accounts/:id/auth/password { password } — 2FA
POST /v1/accounts/:id/auth/logout логаут (сессия отзывается в Telegram)
GET /v1/accounts/:id/auth/status { status, next?, me? }
# Профиль
GET /v1/accounts/:id/me getMe
POST /v1/accounts/:id/me JSON { firstName, lastName, about }
либо multipart с полем avatar
GET /v1/accounts/:id/status { online, status }
POST /v1/accounts/:id/status { online } — присутствие
# Диалоги и чаты
GET /v1/accounts/:id/dialogs?limit&archived&query&offsetDate&offsetId&offsetPeer
-> { dialogs[], next } — у диалогов есть canPost,
participantsCount, forum, noforwards
GET /v1/accounts/:id/chat/:peer информация о чате
GET /v1/accounts/:id/chat/:peer/history?limit&offsetId&offsetDate&minId&maxId&reverse
-> { messages[], nextOffsetId }
(offsetDate в миллисекундах, как и все остальные даты в этом API)
# Сообщения
POST /v1/accounts/:id/chat/:peer/messages { text, replyTo?, topMsgId?, quoteText?, quoteOffset?, parseMode?, silent?, linkPreview?, schedule? }
POST /v1/accounts/:id/chat/:peer/files multipart: files[], caption, replyTo, topMsgId, forceDocument, parseMode, silent
PATCH /v1/accounts/:id/chat/:peer/messages/:msgId { text, parseMode?, linkPreview? }
DELETE /v1/accounts/:id/chat/:peer/messages?ids=1,2&revoke=true
POST /v1/accounts/:id/chat/:peer/messages/:msgId/react { emoji }
POST /v1/accounts/:id/chat/:peer/messages/:msgId/pin { silent?, oneSide? } — закрепить
DELETE /v1/accounts/:id/chat/:peer/messages/:msgId/pin открепить сообщение
DELETE /v1/accounts/:id/chat/:peer/pin?topMsgId= открепить все сообщения (или в топике)
POST /v1/accounts/:id/chat/:peer/read { maxId }
POST /v1/accounts/:id/chat/:peer/forward { ids, fromPeer }
POST /v1/accounts/:id/chat/:peer/messages/copy { fromPeer, msgIds[], caption?, parseMode? }
без штампа «Переслано от»; noforwards источника -> 409 protected_content
GET /v1/accounts/:id/chat/:peer/avatar?size=small|big скачать аватар чата/пользователя (ETag)
GET /v1/accounts/:id/chat/:peer/messages/:msgId/file скачать медиа (Range)
GET /v1/accounts/:id/chat/:peer/messages/:msgId/thumb?size=s|m превью (ETag):peer — @username, username, числовой ID (-1001234567890) или me.
Значение подставляйте через encodeURIComponent.
Медиа
GET …/messages/:msgId/file отдаёт файл потоком и понимает Range: ответ на
диапазон — 206 с Content-Range, на запрос за пределами файла — 416.
Куски файла тянутся из Telegram несколькими сразу — через прокси это заметно
быстрее, чем по одному за раз. Одновременных загрузок на аккаунт не больше
шести, остальные ждут очереди. Оборвавшееся соединение прекращает и загрузку
из Telegram.
GET …/messages/:msgId/thumb?size=s|m отдаёт JPEG-обрезку: s — самую
мелкую, m — самую мелкую из достаточно чётких для пузыря (по длинной
стороне от 1280 px). У ответа сильный ETag; при совпадении
If-None-Match возвращается 304, и обрезка не качается из Telegram вовсе.
У вложения без обрезок — 404 no_thumb; мгновенное размытое превью в этом
случае лежит прямо в сообщении, в поле media.stripped.
Само сообщение теперь описывает вложение целиком: media.{kind, mimeType,
fileName, size, width, height, duration, waveform, thumbs, stripped,
downloadable}. Размеры известны до загрузки — заглушку можно нарисовать
сразу. Рядом появились chatId (всегда маркированный), groupedId
(альбомы), fwdFrom, viaBotId и senderName.
WebSocket
ws://127.0.0.1:3111/v1/ws?accountId=<id>&token=<apiToken>&since=<seq>&stream=<streamId>Подключение поднимает клиента Telegram для аккаунта, если он ещё не поднят. Один аккаунт может держать несколько сокетов — поток получают все.
since=<seq> (опционально): переподключение — отдаёт буферизованные события с
seq > since перед живым потоком. Буфер держит последние 500 событий на
аккаунт; если хвост уже вытеснен, вместо тихого пропуска клиент получает
since_gap (latestSeq) и должен долить историю через REST.
stream=<streamId> (опционально, вместе с since): seq снова считается с 1
при каждом пересоздании буфера (рестарт процесса), поэтому голый since из
прошлого запуска может указывать на совсем другие события. Первый кадр любого
подключения — hello с текущим streamId; его нужно хранить рядом с курсором и
передавать обратно. При несовпадении сервер шлёт since_gap и отдаёт весь буфер.
События (JSON, все с accountEvent: true):
| type | Полезная нагрузка |
|---|---|
| hello | accountId, streamId, latestSeq — всегда первый кадр, идентифицирует поток seq |
| connected | accountId, streamId, seq — подтверждение подписки, последний seq в буфере |
| since_gap | accountId, streamId, latestSeq — буфер уже не покрывает запрошенный диапазон since или stream из другого запуска |
| new_message / edited_message | message — нормализованное сообщение |
| deleted_messages | peerId, deletedIds |
| typing | chatId, userId, action |
| reactions | chatId, msgId, topMsgId, reactions[] — реакции на сообщении |
| pinned_messages | chatId, pinned, messages[] — закрепление/открепление сообщений |
| user_status | userId, status, online, wasOnline, expires — статус пользователя |
| read_inbox | peerId, maxId — мы прочитали чужие сообщения |
| read_outbox | peerId, maxId — собеседник прочитал наши |
| session_closed | reason — логаут, сокет закрывается кодом 4003 |
| error | error — сессия недоступна, сокет закрывается кодом 4002 |
Обе границы прочитанного приходят и с каждым диалогом в GET /dialogs
(readInboxMaxId, readOutboxMaxId). Событие приходит однократно: клиент,
который в тот момент не слушал — или ещё не был установлен, — иначе не узнает,
что собеседник уже прочитал.
Коды закрытия: 4001 — неверный токен или аккаунт не авторизован,
4002 — сессия недоступна, 4003 — логаут.
MCP
apiGram работает и как MCP-сервер: AI-агент (Claude и другие) управляет Telegram-аккаунтом напрямую — читает чаты, отправляет сообщения и файлы, ставит реакции, пересылает — без отдельного слоя интеграции между агентом и шлюзом.
POST/GET/DELETE http://127.0.0.1:3111/v1/accounts/<id>/mcp
Authorization: Bearer <apiToken>Streamable HTTP-транспорт (спецификация).
Тот же bearer-токен и та же привязка к аккаунту, что и у REST — одна
MCP-сессия всегда работает от имени одного аккаунта, и клиенту нужны ровно
accountId и apiToken из Быстрого старта выше. Больше
ничего заводить не нужно: ни отдельных MCP-credentials, ни allow-листа.
Скилл для AI-агента
Хотите, чтобы AI-агент уже знал всё это — включая интерактивный мастер
настройки, который сам найдёт инстанс, создаст/авторизует аккаунт и
подключится? Поставьте скилл
apigram-mcp через
skills.sh:
npx skills add emaxe/apiGramЭто ставит скилл сразу для ~20 агентов (Claude Code, Cursor, Codex, Cline,
Goose и другие) в .agents/skills/apigram-mcp текущего проекта — флаг -g
поставит его глобально.
Подключить MCP-клиента
Подойдёт любой клиент, умеющий Streamable HTTP и произвольные заголовки. Для Claude Desktop или Claude Code — добавить в конфиг MCP клиента:
{
"mcpServers": {
"apigram": {
"type": "http",
"url": "http://127.0.0.1:3111/v1/accounts/acc_.../mcp",
"headers": { "Authorization": "Bearer tok_..." }
}
}
}Если шлюз недоступен агенту локально — укажите в url публичный адрес.
Bearer-токен — единственное, что отделяет агента от этого аккаунта, поэтому
храните его так же секретно, как любой другой apiToken.
Поговорить руками
Рукопожатие — обычный JSON-RPC 2.0 поверх HTTP, удобно проверить деплой без MCP-клиента:
MCP=http://127.0.0.1:3111/v1/accounts/$ACC/mcp
AUTH="Authorization: Bearer $TOKEN"
ACCEPT='Accept: application/json, text/event-stream'
JSON='Content-Type: application/json'
# 1. Инициализация — id сессии приходит в заголовке mcp-session-id
SID=$(curl -sD - -o /dev/null -X POST $MCP -H "$AUTH" -H "$ACCEPT" -H "$JSON" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}' \
| tr -d '\r' | grep -i '^mcp-session-id:' | cut -d' ' -f2)
# 2. Подтверждение рукопожатия — обязательно по протоколу, тела в ответе нет
curl -s -o /dev/null -X POST $MCP -H "$AUTH" -H "$ACCEPT" -H "$JSON" -H "mcp-session-id: $SID" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
# 3. Список доступных tools
curl -s -X POST $MCP -H "$AUTH" -H "$ACCEPT" -H "$JSON" -H "mcp-session-id: $SID" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# 4. Вызов одного из них
curl -s -X POST $MCP -H "$AUTH" -H "$ACCEPT" -H "$JSON" -H "mcp-session-id: $SID" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"send_message","arguments":{"peer":"me","text":"привет из MCP"}}}'
# 5. Закрыть сессию по завершении
curl -s -X DELETE $MCP -H "$AUTH" -H "mcp-session-id: $SID"Tools
| Tool | Описание |
|---|---|
| list_dialogs | Список чатов: limit, archived, query |
| get_chat | Карточка чата/пользователя по peer |
| get_history | История сообщений: peer, limit, offsetId, reverse |
| send_message | Отправка текста: peer, text, replyTo, parseMode, silent, linkPreview |
| edit_message | Редактирование текста: peer, messageId, text |
| delete_messages | Удаление: peer, ids[], revoke |
| mark_as_read | Отметка прочитанным до maxId (0 — всё) |
| react | Эмодзи-реакция: peer, messageId, emoji |
| forward_messages | Пересылка: toPeer, ids[], fromPeer |
| send_files | Отправка до 10 файлов как base64: peer, files[], caption |
| download_file | Метаданные и ссылка на REST-скачивание вложения — не сами байты |
peer везде принимает одно и то же: @username, юзернейм без собачки,
числовой ID или me.
download_file никогда не отдаёт байты внутрь MCP-ответа — только ссылку на
уже существующий GET .../chat/:peer/messages/:msgId/file с тем же
bearer-токеном: стриминг с поддержкой Range там уже реализован, а заворачивать
байты обратно в результат tool означало бы раздувать контекст агента.
Ошибки инструментов приходят как isError: true в результате tool, с тем же
телом { error, message }, что и у REST (Ошибки) — один словарь
ошибок на оба интерфейса, ничего MCP-специфичного учить не нужно.
Ошибки
{ error, message, step?, hint?, seconds? }.
| Статус | Когда |
|---|---|
| 400 | неверные данные или нарушен порядок шагов логина (step укажет шаг) |
| 401 | нет/неверный apiToken |
| 403 | нет доступа к чату |
| 404 | чат, сообщение или медиа не найдены |
| 409 | аккаунт не авторизован или сессия отозвана |
| 429 | flood_wait, в seconds — сколько ждать |
| 502 | прокси отказал или оборвал туннель (proxy_unreachable, proxy_auth_required, proxy_forbidden, proxy_connect_failed, proxy_protocol_error) |
| 504 | proxy_timeout — прокси не ответил за PROXY_TIMEOUT |
Браузерные клиенты
По умолчанию CORS выключен, и ни одна веб-страница обратиться к шлюзу не может. Это состояние по умолчанию выбрано осознанно: шлюз держит боевые сессии Telegram.
Чтобы разрешить веб-клиента, перечислите источники:
CORS_ORIGINS=http://127.0.0.1:8080Список, а не *. При пустом ADMIN_TOKEN эндпоинт POST /v1/accounts открыт,
поэтому со звёздочкой любая посещённая пользователем страница смогла бы создавать
аккаунты на его локальном шлюзе. Значение * поддержано, но выводит предупреждение
при старте.
Проверка Origin распространяется и на WebSocket: правила CORS на рукопожатие
не действуют, поэтому источник сверяется вручную. Клиенты, не присылающие
Origin (curl, мобильные и десктопные сборки, scripts/smoke.mjs),
работают как прежде — ни проверка, ни заголовки их не касаются.
Прокси
MTProto можно пустить через прокси. Одна переменная покрывает все схемы; авторизация везде
опциональна, спецсимволы в пароле кодируются percent-encoding (@ → %40, : → %3A):
PROXY_URL=socks5://user:[email protected]:1080 # и socks4://
PROXY_URL=http://user:[email protected]:3128 # HTTP CONNECT
PROXY_URL=https://127.0.0.1:8443 # то же, но TLS до самого прокси
PROXY_URL=mtproxy://<секрет>@1.2.3.4:443 # прокси Telegram (или ?secret=…)
PROXY_TIMEOUT=5 # таймаут подключения, секундыsocks4/socks5 и mtproxy идут штатным транспортом teleproto. http/https библиотека не
умеет вовсе, поэтому реализованы здесь — туннелем CONNECT поверх node:net / node:tls, без
новых зависимостей. Самоподписанный сертификат прокси принимается только по явному
https://host:8443?insecure=1.
Прокси общий: через него ходят все аккаунты, включая скачивание медиа из других дата-центров. Битое значение роняет старт, а не откатывается молча на прямое соединение — это была бы утечка настоящего IP. Пароль и секрет MTProxy в логи не попадают никогда.
Системные https_proxy / all_proxy / http_proxy читаются только при
PROXY_FROM_ENV=true, в этом порядке и в любом регистре; PROXY_URL в любом случае важнее.
Включение явное, потому что такие переменные часто выставлены в шелле для посторонних задач,
а шлюз с боевыми сессиями Telegram не должен уходить туда молча. В строке при старте видно,
из какой переменной взяты настройки:
apiGram proxy: socks5://10.0.0.9:1080 (без авторизации) (из all_proxy, таймаут 5 с)Безопасность
- Сессия — это учётные данные. В
data/accounts.jsonлежатsessionString, дающие полный доступ к аккаунтам Telegram. Файл пишется с правами0600, весь каталогdata/в.gitignore. Не коммитьте его и не кладите в синхронизируемую папку. ADMIN_TOKENзакрывает создание аккаунтов. Пока он пуст,POST /v1/accountsоткрыт всем, кто достучится до порта. Это допустимо только на127.0.0.1; при старте на другом адресе без токена сервер печатает предупреждение.apiTokenпоказывается ровно один раз — в ответеPOST /v1/accounts. Восстановить его нельзя: потеряли токен — удаляйте аккаунт и создавайте заново.LOG_UPDATES=trueпишет тексты сообщений на диск (data/updates.jsonl). По умолчанию выключено; включайте, только если действительно нужен журнал.- Нет TLS. Перед выставлением наружу ставьте шлюз за обратный прокси. CORS есть, но по умолчанию выключен — см. «Браузерные клиенты».
Ограничения
- Сессии хранятся в
StringSessionбез кэша сущностей: после рестарта обращение к чату по числовому ID может вернутьpeer_not_found— сначала вызовитеGET /dialogs, это прогреет кэш. - Регистрация новых номеров, звонки, секретные чаты и вход по email не поддерживаются.
- Прокси общий: все аккаунты ходят через одно соединение, настройки на аккаунт нет.
- Системные
HTTPS_PROXY/ALL_PROXY/HTTP_PROXYигнорируются, пока не выставленPROXY_FROM_ENV=true. Их часто задают в шелле для посторонних задач, и боевые сессии Telegram не должны уходить туда молча;PROXY_URLв любом случае важнее. В строке при старте видно, из какой переменной взяты настройки. - HTTP-прокси обязан поддерживать
CONNECT; прокси, разрешающие его только на порт 443, отвечаютproxy_forbidden. Ошибки прокси отдаются как502(504при таймауте). - У
mtproxy://нет собственного таймаута подключения: teleproto на этом пути игнорируетPROXY_TIMEOUT, и мёртвый MTProxy держит соединение до таймаута ОС (~75 с). - CORS по умолчанию выключен: браузерный клиент не достучится до шлюза, пока его источник
не перечислен в
CORS_ORIGINS— см. «Браузерные клиенты».
Тесты
npm testВнимание: тесты реестра сейчас пишут в боевой data/accounts.json (см. test/unit.test.js).
Changelog
См. CHANGELOG.md.
Лицензия
ISC
