diff --git a/internal/handler/notify.go b/internal/handler/notify.go new file mode 100644 index 0000000..310651a --- /dev/null +++ b/internal/handler/notify.go @@ -0,0 +1,138 @@ +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 +}