tinyrelayfile
tinyrelay/internal/daemon/git_outbox.go/

internal/daemon/git_outbox.go raw
1package daemon
2
3import (
4 "context"
5 "errors"
6 "net/url"
7 "sort"
8 "strconv"
9 "strings"
10 "time"
11
12 "github.com/FelineStateMachine/tinyrelay/internal/event"
13 "github.com/FelineStateMachine/tinyrelay/internal/replication"
14)
15
16const (
17 gitOutboxAuthorLimit = 32
18 gitOutboxRelayLimit = 24
19)
20
21// gitOutboxOptions describes the bounded, read-only part of GRASP-03. Seeds
22// are the repository's announced relays. Private repositories must also set
23// PrivatePeers; discovered public relays are never used for private work.
24type gitOutboxOptions struct {
25 Transport replication.PullTransport
26 Seeds []string
27 Private bool
28 PrivatePeers []string
29 AuthorOffset int
30 SeedOffset int
31}
32
33// discoverGitOutboxes asks announced relays for the NIP-65 lists of authors
34// who took part in a repository conversation. A failed author or seed is
35// isolated so one unavailable relay cannot prevent the remaining discussion
36// from being imported.
37func discoverGitOutboxes(ctx context.Context, options gitOutboxOptions, authors []string) ([]string, error) {
38 items, err := discoverGitOutboxEvents(ctx, options, authors)
39 result := make([]string, 0, gitOutboxRelayLimit)
40 seen := make(map[string]struct{}, gitOutboxRelayLimit)
41 for _, item := range items {
42 for _, relay := range gitOutboxRelayTags(item) {
43 if !gitOutboxAllowedRelay(relay, options.Private, options.PrivatePeers) {
44 continue
45 }
46 canonical := normalizeRelayGit(relay)
47 if _, ok := seen[canonical]; ok {
48 continue
49 }
50 seen[canonical] = struct{}{}
51 result = append(result, canonical)
52 if len(result) == gitOutboxRelayLimit {
53 return result, err
54 }
55 }
56 }
57 return result, err
58}
59
60// discoverGitOutboxEvents returns the newest valid authored list of each
61// supported kind. Callers should ingest these events with OriginImport so the
62// relay list becomes available after a restart and to normal replication.
63func discoverGitOutboxEvents(ctx context.Context, options gitOutboxOptions, authors []string) ([]event.Event, error) {
64 if options.Transport == nil {
65 return nil, errors.New("GRASP-03: outbox transport is required")
66 }
67 authors = boundedGitAuthorsFrom(authors, options.AuthorOffset)
68 seeds := boundedGitRelaysFrom(options.Seeds, options.Private, options.PrivatePeers, options.SeedOffset)
69 if len(authors) == 0 || len(seeds) == 0 {
70 return nil, nil
71 }
72 newest := make(map[string]event.Event, len(authors)*3)
73 var queryErrors []error
74 workCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
75 defer cancel()
76 stop := false
77 for _, seed := range seeds {
78 for _, author := range authors {
79 for _, kind := range []int{event.KIND_PROFILE, 10002, 10317} {
80 if err := workCtx.Err(); err != nil {
81 queryErrors = append(queryErrors, err)
82 stop = true
83 break
84 }
85 items, err := options.Transport.Query(workCtx, seed, gitOutboxFilter(author, kind))
86 if err != nil {
87 if len(queryErrors) < 8 {
88 queryErrors = append(queryErrors, err)
89 }
90 continue
91 }
92 for _, item := range items {
93 if event.Validate(item) != nil || item.PubKey != author || item.Kind != kind {
94 continue
95 }
96 key := author + ":" + strconv.Itoa(item.Kind)
97 if current, ok := newest[key]; !ok || item.CreatedAt > current.CreatedAt || (item.CreatedAt == current.CreatedAt && item.ID < current.ID) {
98 newest[key] = item
99 }
100 }
101 }
102 if stop {
103 break
104 }
105 }
106 if stop {
107 break
108 }
109 }
110 result := make([]event.Event, 0, len(newest))
111 for _, item := range newest {
112 result = append(result, item)
113 }
114 sort.Slice(result, func(i, j int) bool {
115 if result[i].PubKey == result[j].PubKey {
116 return result[i].Kind < result[j].Kind
117 }
118 return result[i].PubKey < result[j].PubKey
119 })
120 return result, errors.Join(queryErrors...)
121}
122
123func gitOutboxFilter(author string, kind int) event.Filter {
124 limit := 1
125 return event.Filter{Authors: []string{author}, Kinds: []int{kind}, Tags: map[string][]string{}, Limit: &limit}
126}
127
128// gitConversationAuthors returns the repository participants whose outboxes
129// may contain replies. The caller supplies events already admitted to the
130// repository conversation, so arbitrary relay authors cannot expand the
131// discovery set.
132func gitConversationAuthors(events []event.Event, owner string, maintainers []string) []string {
133 authors := make([]string, 0, len(maintainers)+1+len(events))
134 appendAuthor := func(author string) {
135 if len(author) != 64 || strings.Trim(author, "0123456789abcdefABCDEF") != "" {
136 return
137 }
138 for _, existing := range authors {
139 if existing == author {
140 return
141 }
142 }
143 authors = append(authors, author)
144 }
145 appendAuthor(owner)
146 for _, maintainer := range maintainers {
147 appendAuthor(maintainer)
148 }
149 for _, item := range events {
150 switch item.Kind {
151 case event.KIND_GIT_PATCH, event.KIND_GIT_PR, event.KIND_GIT_PR_UPDATE, event.KIND_GIT_ISSUE, 1111, 1630, 1631, 1632, 1633:
152 appendAuthor(item.PubKey)
153 }
154 }
155 return authors
156}
157
158func boundedGitAuthors(authors []string) []string {
159 return boundedGitAuthorsFrom(authors, 0)
160}
161
162func boundedGitAuthorsFrom(authors []string, offset int) []string {
163 seen := make(map[string]struct{}, len(authors))
164 result := make([]string, 0, minGitInt(len(authors), gitOutboxAuthorLimit))
165 if offset < 0 {
166 offset = 0
167 }
168 valid := 0
169 for _, author := range authors {
170 if len(author) != 64 || strings.Trim(author, "0123456789abcdefABCDEF") != "" {
171 continue
172 }
173 if _, ok := seen[author]; ok {
174 continue
175 }
176 seen[author] = struct{}{}
177 if valid < offset {
178 valid++
179 continue
180 }
181 valid++
182 result = append(result, author)
183 if len(result) == gitOutboxAuthorLimit {
184 break
185 }
186 }
187 return result
188}
189
190func boundedGitRelays(relays []string, private bool, peers []string) []string {
191 return boundedGitRelaysFrom(relays, private, peers, 0)
192}
193
194func boundedGitRelaysFrom(relays []string, private bool, peers []string, offset int) []string {
195 seen := make(map[string]struct{}, len(relays))
196 valid := make([]string, 0, len(relays))
197 for _, relay := range relays {
198 if !gitOutboxAllowedRelay(relay, private, peers) {
199 continue
200 }
201 canonical := normalizeRelayGit(relay)
202 if _, ok := seen[canonical]; ok {
203 continue
204 }
205 seen[canonical] = struct{}{}
206 valid = append(valid, canonical)
207 }
208 if len(valid) == 0 {
209 return nil
210 }
211 start := offset % len(valid)
212 if start < 0 {
213 start = 0
214 }
215 rotated := append(append([]string(nil), valid[start:]...), valid[:start]...)
216 result := rotated
217 if len(result) > gitOutboxRelayLimit {
218 result = result[:gitOutboxRelayLimit]
219 }
220 return result
221}
222
223func gitOutboxRelayTags(item event.Event) []string {
224 name := ""
225 switch item.Kind {
226 case 10002:
227 name = "r"
228 case 10317:
229 name = "g"
230 default:
231 return nil
232 }
233 result := make([]string, 0)
234 for _, tag := range item.Tags {
235 if len(tag) < 2 || tag[0] != name {
236 continue
237 }
238 if item.Kind == 10002 && len(tag) > 2 && strings.EqualFold(tag[2], "read") {
239 continue
240 }
241 result = append(result, tag[1])
242 }
243 return result
244}
245
246func gitOutboxAllowedRelay(raw string, private bool, peers []string) bool {
247 u, err := url.Parse(strings.TrimSpace(raw))
248 if err != nil || u.User != nil || u.Fragment != "" || u.Host == "" {
249 return false
250 }
251 if u.Scheme == "http" {
252 u.Scheme = "ws"
253 } else if u.Scheme == "https" {
254 u.Scheme = "wss"
255 }
256 if (u.Scheme != "ws" && u.Scheme != "wss") || !safeGitOutboxHost(u.Hostname()) {
257 return false
258 }
259 if !private {
260 return true
261 }
262 return privatePeerMatch(u.String(), peers)
263}
264
265func safeGitOutboxHost(host string) bool {
266 return host != "" && !strings.ContainsAny(host, " \t\r\n")
267}
268
269func minGitInt(a, b int) int {
270 if a < b {
271 return a
272 }
273 return b
274}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs