feat: subscribe to RabbitMQ trade signals and notify by rules
Consume configurable queues, format signals (including period), share NotifyService with HTTP, and drop duplicate bodies within 1h.
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"aiaa-notification-service/internal/condition"
|
||||
"aiaa-notification-service/internal/engine"
|
||||
"aiaa-notification-service/internal/model"
|
||||
)
|
||||
|
||||
var ErrUnprocessable = errors.New("unprocessable")
|
||||
|
||||
type Request struct {
|
||||
Source *model.Source
|
||||
Event string
|
||||
Data map[string]interface{}
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Matched bool
|
||||
Filtered bool
|
||||
Channels []string
|
||||
Reason string
|
||||
}
|
||||
|
||||
type RuleMatcher interface {
|
||||
Match(ctx context.Context, sourceID int, event string) (*model.Rule, error)
|
||||
}
|
||||
|
||||
type TemplateStore interface {
|
||||
GetTemplate(ctx context.Context, id int) (*model.Template, error)
|
||||
}
|
||||
|
||||
type ChannelRouter interface {
|
||||
Route(ctx context.Context, rule *model.Rule, title, content string) []string
|
||||
}
|
||||
|
||||
type MessageLogger interface {
|
||||
CreateMessageLog(ctx context.Context, ml *model.MessageLog) error
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
matcher RuleMatcher
|
||||
templates TemplateStore
|
||||
renderer *engine.Renderer
|
||||
router ChannelRouter
|
||||
logs MessageLogger
|
||||
}
|
||||
|
||||
func NewService(m RuleMatcher, t TemplateStore, r *engine.Renderer, rt ChannelRouter, logs MessageLogger) *Service {
|
||||
return &Service{matcher: m, templates: t, renderer: r, router: rt, logs: logs}
|
||||
}
|
||||
|
||||
func (s *Service) Process(ctx context.Context, req Request) (Result, error) {
|
||||
rule, err := s.matcher.Match(ctx, req.Source.ID, req.Event)
|
||||
if err != nil {
|
||||
return Result{Matched: false}, nil
|
||||
}
|
||||
|
||||
if rule.Conditions != nil {
|
||||
var conds []model.Condition
|
||||
if err := json.Unmarshal(*rule.Conditions, &conds); err != nil {
|
||||
slog.Error("failed to unmarshal rule conditions", "rule_id", rule.ID, "error", err)
|
||||
return Result{}, fmt.Errorf("%w: invalid rule conditions", ErrUnprocessable)
|
||||
}
|
||||
if !condition.Evaluate(conds, req.Data) {
|
||||
return Result{Matched: true, Filtered: true, Reason: "condition not met"}, nil
|
||||
}
|
||||
}
|
||||
|
||||
tmpl, err := s.templates.GetTemplate(ctx, rule.TemplateID)
|
||||
if err != nil {
|
||||
return Result{}, fmt.Errorf("template not found")
|
||||
}
|
||||
|
||||
content, err := s.renderer.Render(tmpl.Content, req.Data)
|
||||
if err != nil {
|
||||
return Result{}, fmt.Errorf("%w: template render failed: %s", ErrUnprocessable, err.Error())
|
||||
}
|
||||
|
||||
title := req.Source.Name + ": " + req.Event
|
||||
channels := s.router.Route(ctx, rule, title, content)
|
||||
|
||||
if s.logs != nil {
|
||||
go func() {
|
||||
payloadJSON, _ := json.Marshal(req.Data)
|
||||
logCtx := context.Background()
|
||||
for _, chName := range channels {
|
||||
ml := &model.MessageLog{
|
||||
RuleID: rule.ID,
|
||||
ChannelID: parseChannelID(chName),
|
||||
Source: req.Source.Name,
|
||||
Event: req.Event,
|
||||
Payload: payloadJSON,
|
||||
Content: content,
|
||||
Status: "pending",
|
||||
}
|
||||
if err := s.logs.CreateMessageLog(logCtx, ml); err != nil {
|
||||
slog.Warn("failed to create message log", "error", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
slog.Info("notification accepted",
|
||||
"source", req.Source.Name,
|
||||
"event", req.Event,
|
||||
"channels", channels,
|
||||
)
|
||||
|
||||
return Result{Matched: true, Channels: channels}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user