package handler import ( "encoding/json" "io" "log/slog" "net/http" "strconv" "strings" "aiaa-notification-service/internal/cache" "aiaa-notification-service/internal/condition" "aiaa-notification-service/internal/engine" "aiaa-notification-service/internal/model" "aiaa-notification-service/internal/parser" "aiaa-notification-service/internal/store" "github.com/gin-gonic/gin" ) type NotifyHandler struct { store *store.Store cache *cache.Cache // used by engine matcher *engine.Matcher renderer *engine.Renderer router *engine.Router } func NewNotifyHandler(s *store.Store, c *cache.Cache, m *engine.Matcher, r *engine.Renderer, rt *engine.Router) *NotifyHandler { return &NotifyHandler{store: s, cache: c, matcher: m, renderer: r, router: rt} } func (h *NotifyHandler) Handle(c *gin.Context) { src := c.MustGet("source").(*model.Source) // 1. Read raw body body, err := io.ReadAll(c.Request.Body) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "read body failed"}) return } // 2. Parse message p, err := parser.NewParser(src.ParseMode, src.ParsePattern) if err != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "parser setup: " + err.Error()}) return } msg, err := p.Parse(body) if err != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "parse failed: " + err.Error()}) return } // 3. Match rule rule, err := h.matcher.Match(c.Request.Context(), src.ID, msg.Event) if err != nil { // No matching rule → 200 with matched: false c.JSON(http.StatusOK, gin.H{"matched": false}) return } // 4. Evaluate conditions if rule.Conditions != nil { var conds []model.Condition if err := json.Unmarshal(*rule.Conditions, &conds); err == nil { if !condition.Evaluate(conds, msg.Data) { c.JSON(http.StatusOK, gin.H{ "matched": true, "filtered": true, "reason": "condition not met", }) return } } } // 5. Get template content tmpl, err := h.store.GetTemplate(c.Request.Context(), rule.TemplateID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "template not found"}) return } // 6. Render template content, err := h.renderer.Render(tmpl.Content, msg.Data) if err != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "template render failed: " + err.Error()}) return } // 7. Route to channels title := src.Name + ": " + msg.Event channels := h.router.Route(c.Request.Context(), rule, title, content) // 8. Log message (best effort) go func() { payloadJSON, _ := json.Marshal(msg.Data) for _, chName := range channels { chID := parseChannelID(chName) ml := &model.MessageLog{ RuleID: rule.ID, ChannelID: chID, Source: src.Name, Event: msg.Event, Payload: payloadJSON, Content: content, Status: "pending", } _ = h.store.CreateMessageLog(c.Request.Context(), ml) } }() slog.Info("notification accepted", "source", src.Name, "event", msg.Event, "channels", channels, ) c.JSON(http.StatusOK, gin.H{ "matched": true, "channels": channels, "accepted": true, }) } // parseChannelID extracts the numeric channel ID from a channel name formatted as "type:id". func parseChannelID(chName string) int { idx := strings.LastIndex(chName, ":") if idx < 0 || idx == len(chName)-1 { return 0 } id, err := strconv.Atoi(chName[idx+1:]) if err != nil { return 0 } return id }