Перейти к содержанию

Чаты

API для работы с чатами LocalHub позволяет получить список доступных аккаунту групповых чатов, подписать робота на конкретный чат и получать новые сообщения и события через механизм обновлений.

Что важно знать

  • Все методы требуют scope chats.read.
  • Сейчас поддерживаются только групповые чаты.
  • Робот видит чат и может быть подписан на него, если владелец-аккаунт либо имеет право добавлять администраторов в этот чат, либо является создателем чата. В остальных случаях чат недоступен (404 chat_not_found).
  • На текущей стадии проекта к одному чату может быть привязан только один робот. Чтобы сменить робота, ранее подключённого к чату, владелец должен сначала оформить отписку от того робота, которому принадлежит подписка: DELETE /v1/chats/{chat_id}/subscribe. После этого можно подписать другого робота.
  • Подписка действует с момента создания: сообщения, отправленные в чат до подписки, в обновления не попадают.
  • Клиент подтверждает обработанные обновления параметром offset. Подтверждённые обновления повторно не возвращаются.
  • Неподтверждённые обновления хранятся ограниченное время. Клиент должен регулярно получать обновления и сохранять последний обработанный update_id на своей стороне.
  • На одного робота сервис хранит не более 10 000 неподтверждённых обновлений. При превышении самые старые обновления удаляются.
  • Auto-unsubscribe неактивных роботов: если робот не вызывал POST /v1/chats/updates дольше 7 дней, все его подписки снимаются автоматически и неподтверждённые обновления удаляются. Сервис не присылает явного сигнала — обнаружить пропавшие подписки можно через GET /v1/chats/subscriptions (вернётся пустой список). Чтобы возобновить работу, оформите подписки заново через POST /v1/chats/{chat_id}/subscribe. Простое правило, чтобы избежать auto-unsubscribe — держать активный long-poll-цикл (см. «Рекомендованный режим: long polling» ниже): он считается активностью на каждом возвращённом ответе.
  • На MVP в обновлениях передаётся только текстовая часть сообщений. Вложения (медиа, файлы, стикеры, голосовые) пока не поддерживаются. Если у сообщения LocalHub есть и текст, и медиа — робот увидит только message.text, без признака наличия вложений. Сообщения, у которых нет текстовой части (например, чистое фото), в обновления не попадают.

Список endpoint'ов

Метод Путь Назначение
GET /v1/chats список чатов аккаунта
POST /v1/chats/{chat_id}/subscribe подписаться на чат
DELETE /v1/chats/{chat_id}/subscribe отписаться от чата
GET /v1/chats/subscriptions список активных подписок
POST /v1/chats/updates получить новые обновления

GET /v1/chats — список чатов

Возвращает список чатов аккаунта-владельца, к которым у робота есть доступ. В текущей версии возвращаются только групповые чаты.

Чат попадает в выдачу, только если владелец-аккаунт либо имеет право добавлять администраторов в этот чат, либо является создателем чата. В остальных случаях чат не возвращается.

Scope: chats.read

Response 200:

{
  "chats": [
    {
      "id": "019560a1-aaaa-7def-8901-234567890abc",
      "type": "group",
      "name": "Команда разработки",
      "permission": "ro"
    }
  ]
}
Поле Тип Описание
chats[].id UUID идентификатор чата
chats[].type string тип чата, в текущей версии group
chats[].name string название чата
chats[].permission string права робота на этот чат: ro — чтение, rw — чтение и запись

POST /v1/chats/{chat_id}/subscribe — подписаться

Создаёт подписку робота на обновления чата. После этого новые сообщения и события чата доступны через POST /v1/chats/updates.

Scope: chats.read

Path: chat_id — UUID чата, полученный из GET /v1/chats.

Body: не требуется.

Response 200:

{
  "id": "019dce22-1111-7def-8901-234567890abc",
  "created_at": "2026-04-28T10:00:00Z",
  "chat": {
    "id": "019560a1-aaaa-7def-8901-234567890abc",
    "type": "group",
    "name": "Команда разработки",
    "permission": "ro"
  }
}
Поле Тип Описание
id UUID идентификатор подписки
created_at datetime момент создания подписки (UTC, ISO 8601)
chat object те же поля, что отдаёт GET /v1/chats
chat.id UUID идентификатор чата
chat.type string тип чата (group)
chat.name string название чата
chat.permission string права робота (ro / rw)

Повторный запрос тем же роботом для уже подписанного чата возвращает существующую подписку. Чтобы начать получение обновлений заново с текущего момента, сначала откажитесь от подписки, затем оформите её повторно.

Если подписка на этот чат уже принадлежит другому роботу, в том числе из другого аккаунта, API возвращает 409 chat_already_subscribed.

Response 404 chat_not_found — чат недоступен аккаунту, не поддерживается текущей версией API, либо владелец-аккаунт не является создателем чата и не имеет права добавлять администраторов в этот чат.

Response 409 chat_already_subscribed — чат уже подписан другим роботом. Снимите подписку у текущего робота и повторите запрос. Это ограничение MVP и в дальнейшем будет смягчено.

DELETE /v1/chats/{chat_id}/subscribe — отписаться

Удаляет подписку робота на чат. Если подписки нет, запрос всё равно завершается успешно.

После отписки неподтверждённые обновления по этому чату больше не возвращаются роботу. Последующая повторная подписка начнёт получение новых обновлений с момента её создания.

Scope: chats.read

Path: chat_id — UUID чата.

Response 204 — запрос выполнен успешно, тело ответа пустое.

GET /v1/chats/subscriptions — список подписок

Возвращает все активные подписки текущего робота.

Scope: chats.read

Response 200:

{
  "subscriptions": [
    {
      "id": "019dce22-1111-7def-8901-234567890abc",
      "created_at": "2026-04-28T10:00:00Z",
      "chat": {
        "id": "019560a1-aaaa-7def-8901-234567890abc",
        "type": "group",
        "name": "Команда разработки",
        "permission": "ro"
      }
    }
  ]
}

Каждая подписка содержит вложенный chat с тем же набором полей, что и в POST /v1/chats/{chat_id}/subscribe и GET /v1/chats. Подписки на чаты, к которым доступ владельца потерян, в выдачу не попадают.

POST /v1/chats/updates — получить обновления

Основной метод для чтения сообщений и событий чатов.

Scope: chats.read

Параметры

Передаются в query-string.

Параметр Тип Значение по умолчанию Описание
offset int null Вернуть обновления с update_id >= offset и подтвердить все обновления с update_id < offset. null или 0 — получить доступные неподтверждённые обновления без подтверждения предыдущих.
limit int 100 Максимум обновлений за один ответ. Допустимо 1–100.
chat_id UUID отсутствует Необязательный фильтр по конкретному чату. Подтверждение через offset применяется ко всем обновлениям робота, включая обновления из других чатов.
timeout int 0 Long polling. 0 (по умолчанию) — ответ возвращается немедленно с тем, что есть в буфере. 130 — если буфер пуст после подтверждения, сервер удерживает соединение до появления нового обновления для этого робота или до истечения timeout секунд, после чего возвращает результат (возможно пустой). Значения вне [0, 30]422.

Response 200

{
  "updates": [
    {
      "update_id": 42,
      "chat_id": "019560a1-aaaa-7def-8901-234567890abc",
      "type": "message",
      "received_at": "2026-04-28T12:05:01Z",
      "message": {
        "id": "019560a1-eeee-7def-8901-234567890abc",
        "sender": {
          "id": "019560a1-ffff-7def-8901-234567890abc",
          "login": "alex",
          "name": "Алексей"
        },
        "text": "Привет!",
        "sent_at": "2026-04-28T12:05:00Z"
      }
    },
    {
      "update_id": 43,
      "chat_id": "019560a1-aaaa-7def-8901-234567890abc",
      "type": "member_joined",
      "received_at": "2026-04-28T12:06:00Z",
      "member": {
        "id": "019560a1-dddd-7def-8901-234567890abc",
        "login": "maria",
        "name": "Мария"
      }
    }
  ]
}

Структура обновления

Поле Тип Описание
update_id int монотонно возрастающий идентификатор обновления, уникальный для робота
chat_id UUID идентификатор чата-источника
type string тип события: message, member_joined, member_left
received_at datetime время получения обновления сервисом (UTC)
message object объект сообщения, присутствует только при type=message; содержит вложенный объект sender
member object участник JOIN/LEAVE-события; присутствует при member_joined и member_left. Это сам User-объект {id, login, name} (без вложенного user).

Типы обновлений

message — новое сообщение

Поле message:

Поле Тип Описание
id UUID идентификатор сообщения
sender object отправитель сообщения
text string текст сообщения. На MVP — только текстовая часть; медиа-вложения не передаются (см. «Что важно знать»).
sent_at datetime время отправки сообщения (UTC)

member_joined — участник вступил в чат

member_left — участник покинул чат

Для обоих типов поле member — это сам User-объект (без обёртки).

Объект user / sender / member

Поля message.sender и member имеют одинаковую структуру:

Поле Тип Описание
id UUID UUID пользователя в LocalHub
login string стабильный обязательный логин
name string отображаемое имя; если имя не задано, может совпадать с login

Как получать обновления

offset одновременно задаёт нижнюю границу выдачи и подтверждает ранее обработанные обновления.

  1. Первый запрос выполните без offset или с offset=0.
  2. Обработайте полученные обновления и сохраните максимальный update_id.
  3. Следующий запрос выполните с offset = max(update_id) + 1.
  4. Повторяйте цикл с задержкой между запросами.

Подтверждённое обновление не возвращается повторно

После запроса с offset=N обновления с update_id < N считаются обработанными. Сохраняйте необходимые данные до сдвига offset.

Повторные и параллельные запросы

Запросы с одним и тем же offset возвращают одинаковый набор обновлений. Это безопасно для retry. При параллельных запросах клиент должен дедуплицировать update_id на своей стороне.

Рекомендованный режим: long polling

Параметр timeout (от 1 до 30 секунд) удерживает соединение до появления нового обновления для этого робота или до истечения таймаута. Это рекомендованный режим работы в production: минимальная задержка доставки сообщений, минимум пустых запросов, минимальная нагрузка на rate-limit.

#!/bin/bash
: "${ROBOT_TOKEN:?need ROBOT_TOKEN}"
URL='https://robot.prod.lclhub.ru/v1/chats/updates'
OFFSET=0

while true; do
  RESP=$(curl -fsS -X POST \
    "$URL?offset=$OFFSET&limit=100&timeout=25" \
    -H "Authorization: Bearer $ROBOT_TOKEN" \
    --max-time 35)
  COUNT=$(echo "$RESP" | jq '.updates | length')
  if [ "$COUNT" -gt 0 ]; then
    echo "$RESP" | jq -c '.updates[]'
    MAX=$(echo "$RESP" | jq '[.updates[].update_id] | max')
    OFFSET=$((MAX + 1))
  fi
done
import os

import httpx

TOKEN = os.environ["ROBOT_TOKEN"]
URL = "https://robot.prod.lclhub.ru/v1/chats/updates"

def main() -> None:
    offset = 0
    with httpx.Client(timeout=35) as client:
        while True:
            response = client.post(
                URL,
                params={"offset": offset, "limit": 100, "timeout": 25},
                headers={"Authorization": f"Bearer {TOKEN}"},
            )
            response.raise_for_status()
            updates = response.json()["updates"]
            if updates:
                for update in updates:
                    handle(update)
                offset = max(item["update_id"] for item in updates) + 1

def handle(update: dict) -> None:
    if update["type"] == "message":
        message = update["message"]
        print(f"[{update['chat_id']}] {message['sender']['name']}: {message['text']}")
    elif update["type"] == "member_joined":
        print(f"+ {update['member']['user']['name']} вступил в {update['chat_id']}")
    elif update["type"] == "member_left":
        print(f"- {update['member']['user']['name']} вышел из {update['chat_id']}")

if __name__ == "__main__":
    main()

HTTP-таймаут клиента — больше серверного timeout

Long-poll-запрос может висеть до timeout секунд. Ставьте таймаут HTTP-клиента с запасом 5–10 секунд (--max-time 35, httpx.Client(timeout=35)), иначе клиентская библиотека оборвёт запрос до того, как сервер успеет вернуть ответ.

Подтверждение работает идентично

offset подтверждает обработанные обновления до ожидания, а не после. Поведение offset и limit не зависит от timeout.

Один long-poll-цикл на робота

Параллельные long-poll-запросы одного и того же робота будут получать одинаковые обновления (retry-safe), но впустую расходуют соединения и rate-limit. Поддерживайте ровно один активный цикл.

При потере состояния клиента

Если клиент не сохранил последний offset, выполните запрос без offset. API вернёт доступные неподтверждённые обновления. После их обработки сохраните максимальный update_id и продолжайте цикл с offset = max(update_id) + 1.

Short polling — только для тестирования

Запрос без timeout (или с timeout=0) возвращает ответ немедленно с тем, что есть в буфере. Этот режим не предназначен для production:

  • задержка доставки сообщения ≈ половина poll-интервала (типично 2–3 с);
  • большинство запросов — пустые и быстро съедают rate-limit;
  • нет преимуществ перед long polling в любом сценарии.

Используйте short polling только для локальной отладки (увидеть, что лежит в буфере прямо сейчас) или разовых curl-проверок, где не нужен непрерывный цикл:

# Разовая проверка содержимого буфера, без цикла:
curl -fsS -X POST 'https://robot.prod.lclhub.ru/v1/chats/updates?limit=10' \
  -H "Authorization: Bearer $ROBOT_TOKEN" | jq
# Один запрос для отладки — посмотреть, что в буфере:
import os
import httpx

response = httpx.post(
    "https://robot.prod.lclhub.ru/v1/chats/updates",
    params={"limit": 10},
    headers={"Authorization": f"Bearer {os.environ['ROBOT_TOKEN']}"},
    timeout=10,
)
print(response.json())

Для непрерывного получения сообщений в production всегда используйте long polling из раздела выше.

Типичные ошибки

401 — нет авторизации или токен невалиден

Заголовок Authorization отсутствует, не начинается с Bearer или токен невалиден, отозван либо истёк. Подробности приведены в разделе Быстрый старт.

403 permission_denied

У робота нет scope chats.read. В теле ответа поле details.required_scope содержит недостающее разрешение.

404 chat_not_found

Робот пытается подписаться или работать с чатом, который недоступен аккаунту, не поддерживается текущей версией API, либо для которого владелец-аккаунт не является создателем чата и не имеет права добавлять администраторов.

409 chat_already_subscribed

Другой робот уже подписан на этот чат. Снимите подписку у того робота и повторите запрос.

429 rate_limit_exceeded / monthly_quota_exceeded

Превышен лимит запросов. Увеличьте интервал между опросами, чтобы уменьшить количество запросов в периоды без новых событий.