Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Changelog

## 0.34.0

### ⚠️ Breaking: единый формат ответа (`MultiSendResponse`) на всех точках отправки

Все эндпоинты отправки (`/send`, `/alertmanager`, `/grafana`, `/gitlab`) и CLI
(`send`, `enqueue`) теперь возвращают **единый конверт** `MultiSendResponse` —
даже для одного чата:

```json
{"ok": true,
"results": [{"chat": "a", "sync_id": "..."},
{"chat": "b", "request_id": "...", "queued": true}],
"errors": [{"chat": "c", "error": "resolving chat: ..."}]}
```

Раньше `/send`/`/alertmanager`/`/grafana` отвечали `{"ok":true,"sync_id":"..."}`,
а `/gitlab` — то одиночной, то fan-out-формой. Клиентам, которым нужен прежний
контракт, следует остаться на версии `0.33.x`.

### Added: multi-chat fan-out (`chat_id` через запятую)

- `chat_id=a,b,c` (в теле `/send` или `?chat_id=a,b,c` у вебхуков и CLI)
рассылает сообщение во **все** перечисленные чаты (fan-out, best-effort).
Дубликаты схлопываются, порядок сохраняется, чат и бот резолвятся для каждого
таргета отдельно; в `/send` inline-mentions парсятся резолвером каждого бота.
- **Коды ответа:** `200` — sync (доставлено ≥1 чата), `202` — async (enqueue),
`502` — во все чаты не удалось. Request-level ошибки (битый JSON, пустой
`chat_id`, невалидный `status`, неподдерживаемый media-type) сохраняют прежнюю
форму `{"ok":false,"error":"..."}` с кодами `400`/`415`.
- **Async expand:** в режиме `serve --enqueue` (и в CLI `enqueue`) многочатовый
`chat_id` раскрывается в N независимых сообщений в очереди (по одному на чат)
для per-chat retry/ack без дублей; worker не изменён.

См. [docs/integrations.md](docs/integrations.md#мульти-чат-и-единый-ответ-multisendresponse).
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ curl -X POST http://localhost:8080/api/v1/send \

Сервер автоматически добавляет заголовок `X-Request-ID` к каждому ответу (если клиент не передал свой, генерируется уникальный). Все HTTP-запросы логируются в stderr (метод, путь, статус, время выполнения).

> **Мульти-чат.** `chat_id` можно указать через запятую (`chat_id=a,b,c` в теле `/send`
> или `?chat_id=a,b,c` в вебхуках) — сообщение рассылается во все чаты (fan-out).
> Ответ всех эндпоинтов — единый `MultiSendResponse` с пер-чатовыми `results`/`errors`.
> **⚠️ Это ломающее изменение формата ответа** (раньше `{"ok":true,"sync_id":"..."}`);
> подробности — [docs/integrations.md](docs/integrations.md#мульти-чат-и-единый-ответ-multisendresponse).

Подробнее: [docs/integrations.md](docs/integrations.md)


Expand Down Expand Up @@ -167,7 +173,7 @@ chats:

## Интеграции

В режиме веб-сервера есть методы для интеграции с alertmanager, grafana и gitlab (универсальный приёмник любых событий GitLab с фильтрами `only`/`exclude`, шаблонами по типам и `error_events`). Несколько команд могут делить один GitLab-эндпоинт с изоляцией по своим `X-Gitlab-Token` — см. [senders](docs/integrations.md#изоляция-команд-senders-несколько-токенов) и пример [examples/gitlab/config-senders.yaml](examples/gitlab/config-senders.yaml).
В режиме веб-сервера есть методы для интеграции с alertmanager, grafana и gitlab (универсальный приёмник любых событий GitLab с фильтрами `only`/`exclude`, шаблонами по типам и `error_events`). Несколько команд могут делить один GitLab-эндпоинт с изоляцией по своим `X-Gitlab-Token`; при этом `?chat_id=` работает как фильтр внутри разрешённого набора чатов sender'а (400 на пустой, 403 на выход за scope, эквивалентность alias/UUID) — см. [senders](docs/integrations.md#изоляция-команд-senders-несколько-токенов) и пример [examples/gitlab/config-senders.yaml](examples/gitlab/config-senders.yaml) ([Per-team tokens](examples/gitlab/README.md#per-team-tokens-senders)).

Пример конфига alertmanager:

Expand Down Expand Up @@ -232,6 +238,7 @@ helm install express-botx oci://ghcr.io/lavr/charts/express-botx -f values.yaml
| [docs/integrations.md](docs/integrations.md) | Alertmanager, Grafana, GitLab, примеры |
| [docs/deployment.md](docs/deployment.md) | Docker, Helm, systemd, docker-compose |
| [docs/async-queues.md](docs/async-queues.md) | RabbitMQ, Kafka, архитектура очередей |
| [CHANGELOG.md](CHANGELOG.md) | История изменений (в т.ч. ломающие) |
| [docs/quickstart.md](docs/quickstart.md) | Базовые сценарии настройки |

## Лицензия
Expand Down
4 changes: 2 additions & 2 deletions charts/express-botx/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ apiVersion: v2
name: express-botx
description: eXpress BotX API gateway
type: application
version: 0.29.1
appVersion: "0.33.0"
version: 0.30.0
appVersion: "0.34.0"
home: https://github.com/lavr/express-botx
sources:
- https://github.com/lavr/express-botx
Expand Down
9 changes: 7 additions & 2 deletions docs/async-queues.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ express-botx enqueue --routing-mode catalog --bot alerts --chat-id deploy "Deplo

# Mixed mode (default)
express-botx enqueue --chat-id deploy "Hello"

# Несколько чатов — expand в N сообщений в очереди (по одному на чат)
express-botx enqueue --chat-id deploy,ops-alerts "Hello"
```

### HTTP-сервер
Expand All @@ -123,10 +126,12 @@ express-botx enqueue --chat-id deploy "Hello"
express-botx serve --enqueue --config config.yaml
```

Ответ — `202 Accepted`:
Ответ — `202 Accepted`, единый `MultiSendResponse` (по одному `results`-элементу
на чат; `chat_id` через запятую → N сообщений в очереди):

```json
{"ok": true, "queued": true, "request_id": "0d6d7f87-0a2f-4c5b-b0d4-4d0b705a77e2"}
{"ok": true,
"results": [{"chat": "deploy", "request_id": "0d6d7f87-0a2f-4c5b-b0d4-4d0b705a77e2", "queued": true}]}
```

HTTP payload расширяется полями для direct routing:
Expand Down
35 changes: 29 additions & 6 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,19 @@ cat image.png | express-botx send --file - --file-name image.png
express-botx send --host express.company.ru --bot-id UUID --secret KEY --chat-id UUID "Hello"
```

При успехе утилита завершается молча (exit 0). Ошибки выводятся в stderr (exit 1).
При успехе (один чат) утилита завершается молча (exit 0). Ошибки выводятся в stderr (exit 1).

**Несколько чатов.** `--chat-id` принимает список через запятую
(`--chat-id a,b,c`) → сообщение отправляется во все чаты (fan-out, best-effort).
В human-выводе печатается по строке на чат (`chat: sync_id` или `chat: ERROR ...`);
`--format json` отдаёт единый `MultiSendResponse`
(`{"ok":..,"results":[{chat,sync_id}],"errors":[{chat,error}]}`). Exit-код ≠0
только если упали **все** чаты (частичный отказ — exit 0).

### Флаги

```
--chat-id UUID или алиас целевого чата (опционально при наличии default)
--chat-id UUID или алиас чата; список через запятую (a,b,c) — fan-out; опционально при наличии default
--body-from прочитать сообщение из файла
--file путь к файлу-вложению (или - для stdin)
--file-name имя файла (обязательно при --file -)
Expand Down Expand Up @@ -234,15 +241,28 @@ express-botx enqueue --bot-id UUID --chat-id UUID "Привет, @mention[email:
express-botx enqueue --no-parse --bot-id UUID --chat-id UUID "Текст с @mention[email:...] как есть"
```

При успехе выводит `request_id` (text) или `{"ok":true,"queued":true,"request_id":"..."}` (json).
При успехе выводит `request_id` по строке на чат (text) или единый
`{"ok":..,"results":[{chat,request_id,queued:true}],"errors":[{chat,error}]}`
(json). Exit-код ≠0 возвращается только если **ни один** чат не поставлен в
очередь (аналогично 502 all-fail у сервера).

**Несколько чатов.** `--chat-id` принимает список через запятую → в очередь
кладётся **N отдельных сообщений** (по одному на чат, expand на стороне
producer), выводится N `request_id`. Так каждый чат ретраится/подтверждается
независимо, без дублей; worker при этом не меняется («один чат = одно
сообщение»). Валидация/резолвинг маршрута — «всё или ничего»: если хоть один
чат в списке не проходит (не UUID в direct-режиме, неизвестный alias), команда
завершается ошибкой **до** публикации чего-либо, поэтому повтор не создаёт
дублей на уже успешных чатах. Сама публикация — best-effort: сбой брокера на
одном чате не отменяет остальные, а попадает в `errors[]`.

### Флаги

```
--routing-mode direct | catalog | mixed (по умолчанию: mixed)
--bot-id UUID бота (direct routing)
--bot алиас бота из catalog (catalog/mixed)
--chat-id UUID или алиас чата
--chat-id UUID или алиас чата; список через запятую (a,b,c) — N сообщений в очередь
--body-from прочитать сообщение из файла
--file путь к файлу-вложению (или - для stdin)
--file-name имя файла (обязательно при --file -)
Expand Down Expand Up @@ -306,10 +326,13 @@ express-botx serve --config config.yaml --api-key env:MY_API_KEY
express-botx serve --enqueue --config config.yaml
```

Ответ в async-режиме:
Ответ в async-режиме — единый `MultiSendResponse` c кодом `202` (по одному
`results`-элементу на чат; `chat_id` через запятую раскрывается в N сообщений в
очереди):

```json
{"ok": true, "queued": true, "request_id": "0d6d7f87-0a2f-4c5b-b0d4-4d0b705a77e2"}
{"ok": true,
"results": [{"chat": "deploy", "request_id": "0d6d7f87-0a2f-4c5b-b0d4-4d0b705a77e2", "queued": true}]}
```

HTTP payload расширяется полями `routing_mode` и `bot_id` для direct routing:
Expand Down
23 changes: 18 additions & 5 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ server:
| `template_files` | Мапа `event-ключ → путь к файлу шаблона`. Один ключ нельзя задать и в `templates`, и в `template_files`. |
| `error_events` | Список event-ключей, доставляемых с `notification.status=error` (та же грануляция матчинга). |
| `routes` | Опциональный упорядоченный список правил роутинга. Событие уходит в чаты **всех** совпавших правил (объединение+дедуп), `stop:true` обрывает перебор. Каждое правило: `match` (селектор → паттерны glob/`/regex/`; `event` — по event-ключу), `chats` (непустой список алиасов/UUID), `stop`. Без секции — прежнее поведение (один чат). Подробнее и приоритет чатов — в [docs/integrations.md](integrations.md#роутинг-событий-по-чатам-routes). |
| `senders` | Опциональный список дополнительных входящих токенов с жёсткой привязкой к чатам (изоляция команд). Каждый элемент: `secret`/`secret_token` (ссылка `literal`/`env:`/`vault:`, обязателен) и непустой `chats` (алиасы/UUID существующих чатов). Совпал sender-токен → событие уходит **только** в его `chats`; `?chat_id`, `?bot`, `routes` и `default_chat_id` игнорируются. Глобальные `events.only/exclude`, `templates` и `error_events` применяются как обычно. Дубликаты разрезолвленных токенов (sender↔sender, sender↔`secret`) — ошибка на старте. Подробнее — в [docs/integrations.md](integrations.md#изоляция-команд-senders-несколько-токенов). |
| `senders` | Опциональный список дополнительных входящих токенов с жёсткой привязкой к чатам (изоляция команд). Каждый элемент: `secret`/`secret_token` (ссылка `literal`/`env:`/`vault:`, обязателен) и непустой `chats` (алиасы/UUID существующих чатов). Совпал sender-токен → событие уходит в его `chats`; `?bot`, `routes` и `default_chat_id` игнорируются. `?chat_id` работает как **фильтр внутри scope**: отсутствует → все `chats`; непустой subset → только эти чаты (эквивалентность alias↔UUID); чат вне `chats` → `403`; явно пустой (`?chat_id=`) → `400`. Глобальные `events.only/exclude`, `templates` и `error_events` применяются как обычно. Дубликаты разрезолвленных токенов (sender↔sender, sender↔`secret`) — ошибка на старте. Подробнее — в [docs/integrations.md](integrations.md#изоляция-команд-senders-несколько-токенов). |

## Переменные окружения

Expand Down Expand Up @@ -211,6 +211,14 @@ curl /api/v1/send -d '{"bot":"alert-bot","chat_id":"deploy","message":"!"}'
curl /api/v1/alertmanager?bot=deploy-bot
```

## Несколько чатов (`chat_id` через запятую)

`chat_id` можно задать списком через запятую (`chat_id=a,b,c` в теле `/send` или
`?chat_id=a,b,c` в вебхуках/CLI) — сообщение рассылается во **все** перечисленные
чаты (fan-out, best-effort). Ответ единый для всех эндпоинтов — `MultiSendResponse`
с пер-чатовыми `results`/`errors`; подробности, коды ответа и **ломающее изменение
формата** — в [docs/integrations.md](integrations.md#мульти-чат-и-единый-ответ-multisendresponse).

## Чат по умолчанию

Один чат можно пометить как `default: true`. Он будет использоваться когда `--chat-id` (CLI) или `chat_id` (API) не указан:
Expand All @@ -223,10 +231,15 @@ express-botx config chat set general UUID --no-default # снять помет
express-botx config chat list # покажет (default)
```

Приоритет выбора чата в HTTP-сервере:
- `/send`: `chat_id` из запроса → чат по умолчанию → ошибка
- `/alertmanager`, `/grafana`: `?chat_id=` → `default_chat_id` из конфига вебхука → чат по умолчанию → единственный чат → ошибка
- `/gitlab`: `?chat_id=` → `routes` (все совпавшие правила, объединение+дедуп) → `default_chat_id` → чат по умолчанию → единственный чат → `200 {ignored}`; при совпадении sender-токена (`server.gitlab.senders`) цели — всегда `chats` этого sender'а, остальное игнорируется
Приоритет выбора чата в HTTP-сервере (`chat_id` может быть списком через запятую —
тогда фан-аут во все указанные чаты):
- `/send`: `chat_id` из запроса → чат по умолчанию → пустой `chat_id` даёт `400`.
Резолв конкретного чата/бота, если он не удался, — пер-чатовая ошибка в
`errors[]` (а не общий `400`); если упали все чаты — `502`.
- `/alertmanager`, `/grafana`: `?chat_id=` → `default_chat_id` из конфига вебхука → чат по умолчанию → единственный чат → пустой набор даёт `400`; пер-чатовые сбои — в `errors[]`, всё упало — `502`
- `/gitlab`: `?chat_id=` → `routes` (все совпавшие правила, объединение+дедуп) → `default_chat_id` → чат по умолчанию → единственный чат → `200 {ignored}`; при совпадении sender-токена (`server.gitlab.senders`) цели — `chats` этого sender'а, а `?chat_id=` фильтрует внутри них (subset → только они; вне scope → `403`; явно пустой → `400`), `?bot`/`routes`/`default_chat_id` игнорируются

Ответ всех эндпоинтов — единый `MultiSendResponse` (см. [Мульти-чат](integrations.md#мульти-чат-и-единый-ответ-multisendresponse)).

## Формат host

Expand Down
Loading
Loading