Ctrl K
ComunidadGuías…

server/internal/handlers/webhook_watchtower.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package handlers

import (
	"encoding/json"
	"io"
	"log"
	"net/http"
	"regexp"
	"strings"
	"time"

	"blackbox/server/internal/hub"
	"blackbox/shared/types"
	"github.com/oklog/ulid/v2"
	"gorm.io/gorm"
)

type watchtowerPayload struct {
	Title   string `json:"Title"`
	Message string `json:"Message"`
	Level   string `json:"Level"`
}

var watchtowerParensPattern = regexp.MustCompile(`\([^)]*\)`)

func WebhookWatchtower(database *gorm.DB, h *hub.Hub, incidentCh chan<- types.Entry, shutdown <-chan struct{}) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
		if err != nil {
			RecordWebhookDelivery(database, "watchtower", "", "", "error", "failed to read payload")
			writeError(w, http.StatusBadRequest, "failed to read request body")
			return
		}

		var payload watchtowerPayload
		snippet := payloadSnippet(body)
		if err := json.Unmarshal(body, &payload); err != nil {
			RecordWebhookDelivery(database, "watchtower", snippet, "", "error", "malformed JSON")
			writeError(w, http.StatusBadRequest, "invalid JSON")
			return
		}

		if payload.Message == "" {
			RecordWebhookDelivery(database, "watchtower", snippet, "", "ignored", "Message is required")
			writeError(w, http.StatusBadRequest, "Message is required")
			return
		}

		meta := make(map[string]interface{})
		if payload.Title != "" {
			meta["watchtower.title"] = payload.Title
		}
		if payload.Level != "" {
			meta["watchtower.level"] = payload.Level
		}
		updatedServices := extractWatchtowerServices(payload.Message)
		if len(updatedServices) > 0 {
			meta["watchtower.services"] = updatedServices
		}

		metaBytes, err := json.Marshal(meta)
		if err != nil {
			log.Printf("failed to marshal metadata for watchtower: %v, meta: %+v", err, meta)
			metaBytes = []byte("{}")
		}

		serviceName := "watchtower"

		entry := types.Entry{
			ID:        ulid.Make().String(),
			Timestamp: time.Now().UTC(),
			NodeName:  "webhook",
			Source:    "webhook",
			Service:   serviceName,
			Event:     "update",
			Content:   payload.Message,
			Metadata:  string(metaBytes),
		}
		if err := database.Create(&entry).Error; err != nil {
			RecordWebhookDelivery(database, "watchtower", snippet, "", "error", "failed to save entry")
			writeError(w, http.StatusInternalServerError, "failed to save entry")
			return
		}
		dispatchToIncidentChannelWithShutdown(incidentCh, shutdown, entry)
		if h != nil {
			if msg := MarshalWSMessage("entry", entry); msg != nil {
				h.Broadcast(msg)
			}
		}

		RecordWebhookDelivery(database, "watchtower", snippet, "", "processed", "")
		w.WriteHeader(http.StatusCreated)
	}
}

func extractWatchtowerServices(message string) []string {
	message = strings.TrimSpace(message)
	if message == "" {
		return nil
	}

	candidateSection := message
	idx := strings.Index(candidateSection, ":")
	if idx < 0 {
		return nil
	}
	candidateSection = candidateSection[idx+1:]
	candidateSection = strings.TrimSpace(candidateSection)
	if candidateSection == "" {
		return nil
	}

	parts := strings.FieldsFunc(candidateSection, func(r rune) bool {
		return r == ',' || r == '\n' || r == ';'
	})
	servicesSeen := make(map[string]struct{}, len(parts))
	servicesList := make([]string, 0, len(parts))

	for _, part := range parts {
		part = watchtowerParensPattern.ReplaceAllString(part, "")
		part = strings.TrimSpace(strings.Trim(part, `"'[]`))
		if part == "" {
			continue
		}

		normalized := strings.ToLower(strings.TrimSpace(part))
		if normalized == "" {
			continue
		}
		if _, ok := servicesSeen[normalized]; ok {
			continue
		}
		servicesSeen[normalized] = struct{}{}
		servicesList = append(servicesList, normalized)
	}

	return servicesList
}