feat(webhook): text-wrap payload для chat-bot receivers (stellix pattern)
This commit is contained in:
+77
-1
@@ -6,6 +6,7 @@ import cloud.nstart.terravault.ordinis.domain.webhook.WebhookDelivery;
|
||||
import cloud.nstart.terravault.ordinis.domain.webhook.WebhookDeliveryRepository;
|
||||
import cloud.nstart.terravault.ordinis.domain.webhook.WebhookSubscription;
|
||||
import cloud.nstart.terravault.ordinis.domain.webhook.WebhookSubscriptionRepository;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
@@ -76,6 +77,17 @@ public class WebhookDispatcher {
|
||||
@Value("${ordinis.webhook.allowed-hosts:}")
|
||||
private String allowedHostsCsv;
|
||||
|
||||
/**
|
||||
* Hosts что ожидают text-shaped payload {"text": "..."} вместо raw event JSON
|
||||
* (пример: chat.nstart.cloud Webhook Bot, Mattermost-style incoming hooks).
|
||||
* CSV. Default empty — все subscriptions получают raw JSON. Для chat-bot
|
||||
* URLs выставить hosts через ORDINIS_WEBHOOK_TEXT_WRAP_HOSTS env. Pattern
|
||||
* учится у stellix alert-bridge: receivers что показывают alerts человеку
|
||||
* требуют human-readable text, не структурированный JSON.
|
||||
*/
|
||||
@Value("${ordinis.webhook.text-wrap-hosts:}")
|
||||
private String textWrapHostsCsv;
|
||||
|
||||
public WebhookDispatcher(
|
||||
OutboxEventRepository outboxRepository,
|
||||
WebhookSubscriptionRepository subscriptionRepository,
|
||||
@@ -219,7 +231,26 @@ public class WebhookDispatcher {
|
||||
|
||||
Timer.Sample sample = Timer.start();
|
||||
try {
|
||||
byte[] body = objectMapper.writeValueAsBytes(event.getPayload());
|
||||
// Text-wrap для chat-bot receivers (Mattermost @WebhookBot и подобные).
|
||||
// Stellix pattern: alert получатель видит human-readable string, не raw
|
||||
// JSON. Wrap'аем как {"text": "<rendered>"} если URL host в allowlist.
|
||||
// Без wrap чат-бот получает structured JSON, не показывает в чат
|
||||
// ничего интересного OR вообще rejects.
|
||||
byte[] body;
|
||||
try {
|
||||
URI uri = URI.create(sub.getUrl());
|
||||
String host = uri.getHost() != null ? uri.getHost().toLowerCase() : "";
|
||||
if (shouldTextWrap(host)) {
|
||||
String rendered = renderEventAsText(event);
|
||||
var wrapped = java.util.Map.of("text", rendered);
|
||||
body = objectMapper.writeValueAsBytes(wrapped);
|
||||
} else {
|
||||
body = objectMapper.writeValueAsBytes(event.getPayload());
|
||||
}
|
||||
} catch (IllegalArgumentException uriEx) {
|
||||
// Bad URL — fallback to raw, dispatcher logged elsewhere.
|
||||
body = objectMapper.writeValueAsBytes(event.getPayload());
|
||||
}
|
||||
String signature = HmacSigner.sign(sub.getHmacSecret(), body);
|
||||
|
||||
HttpRequest req = HttpRequest.newBuilder()
|
||||
@@ -271,4 +302,49 @@ public class WebhookDispatcher {
|
||||
}
|
||||
deliveryRepository.save(delivery);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true если URL host матчит {@code ordinis.webhook.text-wrap-hosts}
|
||||
* CSV — payload завернётся в {@code {"text": "..."}} как ожидают
|
||||
* Mattermost-style chat bots. См. stellix/monitoring/alert-bridge
|
||||
* для prior art.
|
||||
*/
|
||||
private boolean shouldTextWrap(String host) {
|
||||
if (host == null || host.isBlank() || textWrapHostsCsv == null
|
||||
|| textWrapHostsCsv.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
for (String h : textWrapHostsCsv.split(",")) {
|
||||
if (host.equalsIgnoreCase(h.trim())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render OutboxEvent в human-readable single-line/multi-line text для
|
||||
* chat-bot delivery. Не задействует i18n templates (которые в
|
||||
* ordinis-notifications) — webhook bridge должен быть self-contained
|
||||
* на случай если notifications module disabled.
|
||||
*
|
||||
* <p>Format: {@code [<eventType>] <дictionary>/<businessKey> by <userId>}
|
||||
* + payload preview if present. Mirrors stellix renderAlert() shape.
|
||||
*/
|
||||
private String renderEventAsText(OutboxEvent event) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("[").append(event.getEventType()).append("]");
|
||||
JsonNode p = event.getPayload();
|
||||
if (p != null && !p.isMissingNode()) {
|
||||
JsonNode dict = p.get("dictionary");
|
||||
JsonNode bk = p.get("businessKey");
|
||||
JsonNode op = p.get("operation");
|
||||
JsonNode maker = p.get("maker");
|
||||
if (dict != null) sb.append(" ").append(dict.asText());
|
||||
if (bk != null) sb.append("/").append(bk.asText());
|
||||
if (op != null) sb.append(" op=").append(op.asText());
|
||||
if (maker != null) sb.append(" by=").append(maker.asText());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user