feat: notify handler — parse, match, evaluate, render, route

This commit is contained in:
2026-06-27 13:22:14 +08:00
parent 127cf12656
commit 2cf6601be0
+138
View File
@@ -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
}