clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/tinyagent
tinyrelay / internal / records / notifications.go
notifications.go 3.9K
1package records23import (4 "context"5 "encoding/json"6 "errors"7 "strings"89 "fiatjaf.com/nostr"10 "fiatjaf.com/nostr/nip44"11 "github.com/FelineStateMachine/tinyrelay/internal/event"12)1314func (s *Service) notify(ctx context.Context, typ, owner, target string, now int64) error {15 if !s.policy().Notify.Succession && (typ == "succession" || typ == "succession-transfer") {16 return nil17 }18 content, _ := json.Marshal(map[string]string{"type": typ, "target": target})19 wrapped, err := s.giftWrap(owner, string(content), "relay", now)20 if err != nil {21 return err22 }23 if s.deliverNotification != nil {24 if err := s.deliverNotification(ctx, wrapped, target); err != nil {25 return err26 }27 return s.push(ctx, target, typ, "relay", "")28 }29 if s.onGenerated != nil {30 return s.onGenerated(ctx, wrapped)31 }32 return err33}3435func (s *Service) push(ctx context.Context, recipient, kind, subject, text string) error {36 if s.pushNotification == nil {37 return nil38 }39 return s.pushNotification(ctx, recipient, kind, subject, text)40}4142// Notify applies notification preferences and emits a durable gift wrap.43func (s *Service) Notify(ctx context.Context, kind, text, subject, target string, now int64) error {44 p := s.policy()45 if kind != "test" {46 enabled := map[string]bool{"reports": p.Notify.Reports, "jobs": p.Notify.Jobs, "succession": p.Notify.Succession, "digest": p.Notify.Digest}[kind]47 if !enabled {48 return nil49 }50 }51 if target == "" {52 target = p.Owner53 }54 return s.notifyText(ctx, kind, text, subject, target, now)55}5657func (s *Service) notifyText(ctx context.Context, kind, text, subject, target string, now int64) error {58 if target == "" {59 return errors.New("notify: empty recipient")60 }61 if s.deliverNotification == nil && s.onGenerated == nil {62 return nil63 }64 wrapped, err := s.giftWrap(target, text, subject, now)65 if err != nil {66 return err67 }68 if s.deliverNotification != nil {69 if err := s.deliverNotification(ctx, wrapped, target); err != nil {70 return err71 }72 return s.push(ctx, target, kind, subject, text)73 }74 if s.onGenerated != nil {75 return s.onGenerated(ctx, wrapped)76 }77 return nil78}7980// giftWrap creates the NIP-17 and NIP-59 records.81func (s *Service) giftWrap(recipient, content, subject string, now int64) (event.Event, error) {82 rpk, err := nostr.PubKeyFromHex(recipient)83 if err != nil {84 return event.Event{}, err85 }86 sk, err := nostr.SecretKeyFromHex(s.secret)87 if err != nil {88 return event.Event{}, err89 }90 // Build the three NIP-59 records with the project's canonical event signer.91 // This avoids the upstream nostr JSON encoder's unsafe fast path under -race.92 tags := [][]string{{"p", recipient}}93 if subject != "" {94 tags = append(tags, []string{"subject", subject})95 }96 rumor := event.Event{CreatedAt: now, Kind: event.KIND_DM, Tags: tags, Content: content}97 if err := event.Sign(&rumor, s.secret); err != nil {98 return event.Event{}, err99 }100 rumor.Sig = strings.Repeat("0", 128)101 rumorRaw, err := json.Marshal(rumor)102 if err != nil {103 return event.Event{}, err104 }105 key, err := nip44.GenerateConversationKey(rpk, sk)106 if err != nil {107 return event.Event{}, err108 }109 rumorCipher, err := nip44.Encrypt(string(rumorRaw), key)110 if err != nil {111 return event.Event{}, err112 }113 seal := event.Event{CreatedAt: now, Kind: 13, Tags: [][]string{}, Content: rumorCipher}114 if err := event.Sign(&seal, s.secret); err != nil {115 return event.Event{}, err116 }117 nonce := nostr.Generate()118 outerKey, err := nip44.GenerateConversationKey(rpk, nonce)119 if err != nil {120 return event.Event{}, err121 }122 sealRaw, err := json.Marshal(seal)123 if err != nil {124 return event.Event{}, err125 }126 outerCipher, err := nip44.Encrypt(string(sealRaw), outerKey)127 if err != nil {128 return event.Event{}, err129 }130 wrapped := event.Event{CreatedAt: now, Kind: event.KIND_WRAP, Tags: [][]string{{"p", recipient}}, Content: outerCipher}131 if err := event.Sign(&wrapped, nonce.Hex()); err != nil {132 return event.Event{}, err133 }134 return wrapped, nil135}