API пользователей бота: доступ и защита ресурсов
Этот контракт относится к GET /api/bot/tokens/:tokenId/users и
GET /api/bot/tokens/:tokenId/users/:userId и
GET /api/bot/tokens/:tokenId/users/:userId/photo. Маршруты возвращают данные
пользователей выбранного токена; доступ к проекту проверяется до чтения
пользователей и получения аватара. Параметр telegram_id сам по себе не
предоставляет доступ: актор определяется существующей проверкой авторизации.
Ограничение частоты запросов
Список, карточка и фотография расходуют общие квоты, поэтому переключение между ними или между токенами не создаёт новый бюджет запросов.
| Проверка | Квота за 60 секунд | Ключ |
|---|---|---|
| Перед проверкой актора | 600 запросов | IP, полученный Express; IPv6 объединяется по /56 |
| После проверки актора | 120 запросов | Авторизованный владелец, включая запросы с impersonation |
Квота владельца не зависит от переданного telegram_id. Учитываются успешные
и ошибочные запросы, дошедшие до соответствующего ограничителя.
IP определяется настройкой Express trust proxy = 1, а не прямым чтением
заголовка X-Forwarded-For. При развёртывании нужно обеспечить ожидаемый путь
через доверенный прокси: доступ клиента напрямую к API может позволить подменить
IP через этот заголовок. Квота авторизованного владельца от такой подмены не зависит.
Превышение возвращает HTTP 429 с JSON:
{
"error": "RATE_LIMIT_EXCEEDED",
"code": "BOT_USERS_RATE_LIMITED",
"message": "Слишком много запросов к пользователям бота. Повторите позже."
}
Retry-After задаёт задержку перед повтором в секундах. Клиент должен соблюдать
её, а не немедленно повторять запрос. Заголовки RateLimit и RateLimit-Policy
передают сведения о квоте в формате draft-7; старые X-RateLimit-* не используются.
При доступном Redis счётчик и срок действия обновляются атомарно и действуют
на все экземпляры API. При недоступном Redis продолжает работать локальная
квота процесса с сохранённым расходом. В этом режиме общий предел между
репликами не гарантируется. Ожидание Redis ограничено 150 мс; после ошибки
повторная попытка откладывается на 5 секунд. Число незавершённых команд ограничено.
Локальное хранилище содержит до 10 000 ключей на квоту: при заполнении новые
ключи получают 429, действующие счётчики не вытесняются.
Параметры и пагинация
tokenId — положительное целое в диапазоне int32, допускается префикс token_.
userId — ненулевое целое в безопасном диапазоне JavaScript, допускается префикс
user_. Отрицательные идентификаторы сохраняют совместимость с группами.
Значения должны быть записаны десятичными цифрами: дроби, экспонента,
частично числовые строки и массивы параметров отклоняются с HTTP 400.
| Параметр списка | По умолчанию | Допустимый диапазон |
|---|---|---|
limit | 10 | 1–50 |
offset | 0 | 0–100 000 |
Выход за диапазон возвращает 400; сервер не обрезает значение до границы.
Ответ сохраняет поля items, count, nextOffset, prevOffset, fromItem,
toItem. Для пустой страницы fromItem и toItem равны нулю. Порядок стабилен
при одинаковой активности: last_interaction DESC, user_id DESC.
Поскольку используется offset-пагинация, изменение активности между запросами
может сместить пользователей между страницами.
Используются существующие способы определения актора: сессия, PAT и контекст
bot_manager. Без актора возвращается 401, при отсутствии токена — 404,
при отсутствии доступа к проекту — 403, при отсутствии пользователя — 404.
Аватары
Карточка возвращает photoFileId (Telegram file_id), photoUrl (адрес
защищённого прокси проекта) и photoDownloadUrl (загрузка через API бота).
Отсутствие фотографии, сбой Telegram или перегрузка возвращают три поля
со значением null, сохраняя данные пользователя.
Авторизация и проверка доступа выполняются до обращения к кэшу.
Получение карточки запрашивает только метаданные через getUserProfilePhotos.
Байты изображения не скачиваются и файлы в uploads не создаются.
photoFileId относится к боту, который получил фотографию: другой бот
не должен использовать его в sendPhoto. Для интеграций нужен доступ
к файлу через соответствующий токен, а не передача чужого file_id.
Кэш метаданных действует внутри одного процесса API и изолирован по проекту, записи токена, пользователю и хешу текущего секрета. Параллельные одинаковые запросы объединяются. Смена секрета создаёт новый ключ.
| Результат | Срок хранения |
|---|---|
Полученный file_id | 1 час |
| Telegram подтвердил отсутствие фотографии | 5 минут |
| Ошибка получения | 30 секунд |
Кэш содержит до 1000 записей и вытесняет давно не использованные. Одновременно разрешены четыре запроса метаданных и до 32 незавершённых обращений; перегрузка возвращает пустые поля без очереди и без сохранения отрицательного результата. У реплик API независимые кэши и ограничения. Запрос метаданных ограничен 10 секундами; JSON — 256 КиБ. Перенаправления запрещены.
Просмотр изображения
photoUrl имеет вид
/api/projects/9/telegram-file?fileId=...&tokenId=7.
Прокси проверяет авторизацию и доступ к проекту при каждом запросе.
Ссылка не является публичной: обычный запрос Telegram по URL без сессии или
PAT её не откроет. Доступ определяется настоящим владельцем сессии/PAT;
подстановка telegram_id через bot_manager сама по себе доступа к этому
маршруту не даёт.
Параметр tokenId привязывает запрос к конкретному токену проекта.
Некорректный формат возвращает 400, отсутствующий или чужой токен — 404
без переключения на токен по умолчанию. Если параметр отсутствует, используется
токен проекта по умолчанию. Знание file_id не заменяет проверку доступа.
Прокси передаёт файл потоком без записи на диск и использует приватное
кэширование. Поддерживаются диапазоны с ответами 206/416.
Для getFile действует ограничение JSON 256 КиБ; перенаправления и небезопасные
пути файлов отклоняются. Для общего прокси срок запроса — 120 секунд;
отключение клиента отменяет обращения к Telegram. Приватное кэширование
разделяет ответы по Cookie и Authorization.
Файлы, созданные прежней реализацией в uploads, автоматически не удаляются.
Их удаление требует отдельного решения с учётом действующих ссылок; переход
на прокси не закрывает доступ к старым публичным URL.
Загрузка другим ботом
photoDownloadUrl имеет вид
/api/bot/tokens/7/users/161/photo?telegram_id=123. Запрос проходит общие
ограничители, определение актора и проверку доступа к токену. Затем проверяется
наличие пользователя и фотография передаётся тем же потоковым прокси,
с ограничением изображения 5 МиБ и потоковой загрузки 10 секундами.
Этот маршрут поддерживает PAT со scope bot_manager и действующим telegram_id.
Отсутствие пользователя или фотографии возвращает 404.
URL не содержит секретов и сам по себе не предоставляет доступ. Интеграция
скачивает его с Authorization: Bearer …, затем отправляет байты через своего
бота. Bot Manager использует ограниченную загрузку в память и BufferedInputFile;
сервер не сохраняет фотографию на диск или как base64 в данных пользователя.
Передача защищённого URL напрямую в sendPhoto не заменяет эту загрузку:
Telegram не добавляет авторизационный заголовок.
Проверки
Целевые регрессионные проверки выполняются командой
npx vitest run --config server/routes/userProjectsTokens/vitest.config.ts.
Она подключена к заданию security процесса Security regressions в CI.
Проверки общего потокового прокси также запускаются в CI командой
npx vitest run --config server/routes/botIntegration/handlers/botData/telegram-file.vitest.config.ts.
См. также справочник API и вкладку пользователей.