tinyrelayfile
tinyrelay/internal/replication/replication.go/

internal/replication/replication.go raw
1package replication
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "net/url"
9 "strings"
10
11 "github.com/FelineStateMachine/tinyrelay/internal/event"
12 "github.com/FelineStateMachine/tinyrelay/internal/storage"
13)
14
15// Origin identifies how an event entered the relay and controls whether it is
16// eligible for outbound fanout.
17type Origin string
18
19const (
20 OriginClient Origin = "client"
21 OriginImport Origin = "import"
22 OriginServer Origin = "server"
23 OriginPush Origin = "push"
24 OriginBackupRestore Origin = "backup_restore"
25)
26
27// Policy supplies current fanout decisions and relay identity. The host
28// controls process capacity independently.
29type Policy struct {
30 Enabled bool
31 ReadMembersOnly bool
32 SelfPubKey string
33 SelfRelayURL string
34}
35
36// RelayDirectory returns write relays for an author and read relays for a
37// recipient. Returned URLs are filtered by PrepareIntents.
38type RelayDirectory interface {
39 WriteRelays(pubkey string) []string
40 ReadRelays(pubkey string) []string
41}
42
43// RelayListDiscovery returns the source-ordered NIP-65 relay list used by
44// owner backfill. RelayDirectory's directional methods remain sufficient for
45// delivery planning.
46type RelayListDiscovery interface {
47 RelayList(pubkey string) []string
48}
49
50// Discovery resolves write relays for an author.
51type Discovery interface {
52 DiscoverRelays(ctx context.Context, pubkey string) ([]string, error)
53}
54
55// ReadDiscovery resolves read relays for a recipient.
56type ReadDiscovery interface {
57 DiscoverReadRelays(ctx context.Context, pubkey string) ([]string, error)
58}
59
60// IntentPayload records the event origin in a delivery intent.
61type IntentPayload struct {
62 Origin Origin `json:"origin"`
63}
64
65// PrepareIntents computes NIP-65 outbox work. The event transaction should
66// pass the returned references to storage.Save as SaveOptions.Intents.
67func PrepareIntents(ctx context.Context, e event.Event, origin Origin, policy Policy, directory RelayDirectory) []storage.Intent {
68 if err := contextErr(ctx); err != nil || !eligible(e, origin, policy) {
69 return nil
70 }
71 targets := uniqueTargets(directory.WriteRelays(e.PubKey))
72 for _, pubkey := range event.TagValues(e, "p") {
73 targets = appendUnique(targets, directory.ReadRelays(pubkey))
74 }
75 payload, err := json.Marshal(IntentPayload{Origin: origin})
76 if err != nil {
77 return nil
78 }
79 intents := make([]storage.Intent, 0, len(targets))
80 for _, target := range targets {
81 if !safeRelayURL(target) {
82 continue
83 }
84 if policy.SelfRelayURL != "" && canonicalRelayURL(target) == canonicalRelayURL(policy.SelfRelayURL) {
85 continue
86 }
87 intents = append(intents, storage.Intent{Kind: "delivery", EventID: e.ID, Target: target, Payload: string(payload)})
88 }
89 return intents
90}
91
92func contextErr(ctx context.Context) error {
93 select {
94 case <-ctx.Done():
95 return ctx.Err()
96 default:
97 return nil
98 }
99}
100
101func eligible(e event.Event, origin Origin, policy Policy) bool {
102 peerPublication := origin == OriginServer && e.Kind == event.KIND_RELAY_DISCOVERY && e.PubKey == policy.SelfPubKey
103 if !policy.Enabled || policy.ReadMembersOnly || (origin != OriginClient && !peerPublication) || (e.PubKey == policy.SelfPubKey && !peerPublication) || privateKind(e.Kind) {
104 return false
105 }
106 for _, tag := range e.Tags {
107 if len(tag) > 0 && tag[0] == "-" {
108 return false
109 }
110 }
111 return true
112}
113
114func privateKind(kind int) bool {
115 return event.IsPrivate(kind) || kind == event.KIND_DM
116}
117
118func uniqueTargets(values []string) []string {
119 out := make([]string, 0, len(values))
120 for _, value := range values {
121 if safeRelayURL(value) {
122 out = appendUnique(out, []string{value})
123 }
124 }
125 return out
126}
127
128func appendUnique(out []string, values []string) []string {
129 for _, value := range values {
130 value = strings.TrimRight(strings.TrimSpace(value), "/")
131 if value == "" || contains(out, value) {
132 continue
133 }
134 out = append(out, value)
135 }
136 return out
137}
138
139func contains(values []string, wanted string) bool {
140 for _, value := range values {
141 if value == wanted {
142 return true
143 }
144 }
145 return false
146}
147
148func safeRelayURL(raw string) bool {
149 u, err := url.Parse(strings.TrimRight(strings.TrimSpace(raw), "/"))
150 if err != nil || (u.Scheme != "ws" && u.Scheme != "wss") || u.Host == "" || u.User != nil || u.Fragment != "" {
151 return false
152 }
153 return true
154}
155
156func canonicalRelayURL(raw string) string {
157 u, err := url.Parse(strings.TrimSpace(raw))
158 if err != nil || u.Host == "" {
159 return strings.TrimRight(strings.TrimSpace(raw), "/")
160 }
161 scheme := strings.ToLower(u.Scheme)
162 switch scheme {
163 case "ws":
164 scheme = "http"
165 case "wss":
166 scheme = "https"
167 }
168 host := strings.ToLower(u.Hostname())
169 if port := u.Port(); port != "" {
170 host += ":" + port
171 }
172 path := strings.TrimRight(u.EscapedPath(), "/")
173 if path == "" {
174 path = "/"
175 }
176 return scheme + "://" + host + path + "?" + u.RawQuery
177}
178
179// DeliveryTransport sends one event to a relay target. The implementation
180// owns connection reuse, request deadlines and transport shutdown.
181type DeliveryTransport interface {
182 Send(ctx context.Context, target string, e event.Event) (DeliveryResult, error)
183}
184
185// DeliveryResult reports the relay's acceptance response. Duplicate
186// acknowledgments are treated as successful delivery by the handlers.
187type DeliveryResult struct {
188 Accepted bool
189 Message string
190}
191
192func ValidateIntent(intent storage.Intent) error {
193 if intent.Kind == "" || intent.EventID == "" || !safeRelayURL(intent.Target) {
194 return errors.New("replication: malformed intent")
195 }
196 return nil
197}
198
199func DecodeIntentPayload(raw string) (IntentPayload, error) {
200 var payload IntentPayload
201 if err := json.Unmarshal([]byte(raw), &payload); err != nil {
202 return payload, fmt.Errorf("replication: decode intent payload: %w", err)
203 }
204 return payload, nil
205}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs