Implement notifications service with shared messaging and email drivers (#36)
This commit is contained in:
1 parent
36277b6c45
commit
5c2d36281b
54 files changed
+3746
-6
No files matched your search
@@ -0,0 +1,109 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/notifications/email"
|
||||
"git.sendico.io/tech/sxchnge/notifications/model"
|
||||
"git.sendico.io/tech/sxchnge/notifications/templates"
|
||||
"git.sendico.io/tech/sxchnge/shared/database/record"
|
||||
merrors "git.sendico.io/tech/sxchnge/shared/errors"
|
||||
mlogger "git.sendico.io/tech/sxchnge/shared/logging"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
Find(context.Context, string, string) (*model.Notification, error)
|
||||
Get(context.Context, string, string) (*model.Notification, error)
|
||||
Create(context.Context, *model.Notification) (*model.Notification, error)
|
||||
Claim(context.Context, string, time.Time) (*model.Notification, error)
|
||||
Finish(context.Context, *model.Notification) error
|
||||
Recover(context.Context, time.Time) error
|
||||
}
|
||||
type Service struct {
|
||||
repo Repository
|
||||
catalog *templates.Catalog
|
||||
sender email.Sender
|
||||
driver string
|
||||
logger mlogger.Logger
|
||||
}
|
||||
|
||||
func New(repo Repository, catalog *templates.Catalog, sender email.Sender, driver string, logger mlogger.Logger) *Service {
|
||||
return &Service{repo, catalog, sender, driver, mlogger.Named(logger, "notifications")}
|
||||
}
|
||||
func (s *Service) Send(ctx context.Context, caller string, r model.Request) (*model.Notification, error) {
|
||||
if caller == "" {
|
||||
return nil, merrors.Unauthorized("missing caller")
|
||||
}
|
||||
if r.Key == "" || len(r.Key) > 128 || r.UserID == "" || len(r.UserID) > 128 || !email.ValidAddress(r.Address) || len(r.TemplateID) > 128 || len(r.TemplateVersion) > 64 || len(r.Locale) > 64 || strings.ContainsRune(r.TemplateID+r.TemplateVersion, 0) {
|
||||
return nil, merrors.InvalidArgument("invalid notification request")
|
||||
}
|
||||
if len(r.Parameters) > 64 {
|
||||
return nil, merrors.InvalidArgument("too many parameters")
|
||||
}
|
||||
for k, v := range r.Parameters {
|
||||
if k == "" || len(k) > 64 || len(v) > 4096 {
|
||||
return nil, merrors.InvalidArgument("invalid template parameter")
|
||||
}
|
||||
}
|
||||
payload, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument("invalid request")
|
||||
}
|
||||
if len(payload) > 64<<10 {
|
||||
return nil, merrors.InvalidArgument("request too large")
|
||||
}
|
||||
sum := sha256.Sum256(payload)
|
||||
fingerprint := hex.EncodeToString(sum[:])
|
||||
existing, err := s.repo.Find(ctx, caller, r.Key)
|
||||
if err == nil {
|
||||
return same(existing, fingerprint)
|
||||
}
|
||||
if !errors.Is(err, merrors.ErrNoData) {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
expiry := r.ExpiresAt
|
||||
if expiry.IsZero() {
|
||||
expiry = now.Add(24 * time.Hour)
|
||||
}
|
||||
if !expiry.After(now) || expiry.After(now.Add(24*time.Hour)) {
|
||||
return nil, merrors.InvalidArgument("expiry must be within the next 24 hours")
|
||||
}
|
||||
rendered, err := s.catalog.Render(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n := &model.Notification{Base: storable.Base{ID: bson.NewObjectID(), CreatedAt: now, UpdatedAt: now}, Caller: caller, Key: r.Key, Fingerprint: fingerprint, UserID: r.UserID, TemplateID: r.TemplateID, TemplateVersion: r.TemplateVersion, Email: rendered, Driver: s.driver, State: model.Queued, ExpiresAt: expiry, NextAttemptAt: now, PurgeAt: now.Add(7 * 24 * time.Hour)}
|
||||
stored, err := s.repo.Create(ctx, n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := same(stored, fingerprint)
|
||||
if err == nil {
|
||||
s.logger.Info("Notification accepted", zap.String("notification_id", stored.ID.Hex()), zap.String("caller", caller))
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
func same(n *model.Notification, fingerprint string) (*model.Notification, error) {
|
||||
if n.Fingerprint != fingerprint {
|
||||
return nil, merrors.DataConflict("idempotency key already used with a different request")
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
func (s *Service) Get(ctx context.Context, caller, id string) (*model.Notification, error) {
|
||||
if caller == "" {
|
||||
return nil, merrors.Unauthorized("missing caller")
|
||||
}
|
||||
if _, err := bson.ObjectIDFromHex(id); err != nil {
|
||||
return nil, merrors.InvalidArgument("invalid notification ID")
|
||||
}
|
||||
return s.repo.Get(ctx, caller, id)
|
||||
}
|
||||
Reference in new issue
Block a user