Чаты¶
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 (по умолчанию) — ответ возвращается немедленно с тем, что есть в буфере. 1–30 — если буфер пуст после подтверждения, сервер удерживает соединение до появления нового обновления для этого робота или до истечения 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 одновременно задаёт нижнюю границу выдачи и подтверждает ранее
обработанные обновления.
- Первый запрос выполните без
offsetили сoffset=0. - Обработайте полученные обновления и сохраните максимальный
update_id. - Следующий запрос выполните с
offset = max(update_id) + 1. - Повторяйте цикл с задержкой между запросами.
Подтверждённое обновление не возвращается повторно
После запроса с 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-проверок, где не нужен непрерывный цикл:
Для непрерывного получения сообщений в 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
Превышен лимит запросов. Увеличьте интервал между опросами, чтобы уменьшить количество запросов в периоды без новых событий.