npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

apigram

v1.5.0

Published

Multi-user Telegram API gateway (MTProto) — REST + WebSocket

Readme

apiGram

npm version node license

Multi-user Telegram API gateway (MTProto) — REST, WebSocket и MCP-сервер для AI-агентов.

Один процесс держит пул TelegramClient по одному на аккаунт; любое клиентское приложение подключается по HTTP и получает realtime-поток по WebSocket — а AI-агент управляет тем же аккаунтом через MCP-инструменты, без отдельного слоя интеграции.

English version: README.md


Содержание

Установка

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