replication_response.go raw
1 package directory
2
3 import (
4 "encoding/json"
5
6 "next.orly.dev/pkg/lol/chk"
7 "next.orly.dev/pkg/lol/errorf"
8 "next.orly.dev/pkg/nostr/encoders/event"
9 "next.orly.dev/pkg/nostr/encoders/tag"
10 )
11
12 // EventResult represents the result of processing a single event in a
13 // replication request.
14 type EventResult struct {
15 EventID string `json:"event_id"`
16 Status ReplicationStatus `json:"status"`
17 Error string `json:"error,omitempty"`
18 }
19
20 // ReplicationResponseContent represents the JSON content of a Directory Event
21 // Replication Response event (Kind 39105).
22 type ReplicationResponseContent struct {
23 RequestID string `json:"request_id"`
24 Results []*EventResult `json:"results"`
25 }
26
27 // DirectoryEventReplicationResponse represents a complete Directory Event
28 // Replication Response event (Kind 39105) with typed access to its components.
29 type DirectoryEventReplicationResponse struct {
30 Event *event.E
31 Content *ReplicationResponseContent
32 RequestID string
33 Status ReplicationStatus
34 ErrorMsg string
35 SourceRelay string
36 }
37
38 // NewDirectoryEventReplicationResponse creates a new Directory Event Replication
39 // Response event.
40 func NewDirectoryEventReplicationResponse(
41 pubkey []byte,
42 requestID string,
43 status ReplicationStatus,
44 errorMsg, sourceRelay string,
45 results []*EventResult,
46 ) (derr *DirectoryEventReplicationResponse, err error) {
47
48 // Validate required fields
49 if len(pubkey) != 32 {
50 return nil, errorf.E("pubkey must be 32 bytes")
51 }
52 if requestID == "" {
53 return nil, errorf.E("request ID is required")
54 }
55 if err = ValidateReplicationStatus(string(status)); chk.E(err) {
56 return
57 }
58 if sourceRelay == "" {
59 return nil, errorf.E("source relay is required")
60 }
61
62 // Create content
63 content := &ReplicationResponseContent{
64 RequestID: requestID,
65 Results: results,
66 }
67
68 // Marshal content to JSON
69 var contentBytes []byte
70 if contentBytes, err = json.Marshal(content); chk.E(err) {
71 return
72 }
73
74 // Create base event
75 ev := CreateBaseEvent(pubkey, DirectoryEventReplicationResponseKind)
76 ev.Content = contentBytes
77
78 // Add required tags
79 ev.Tags.Append(tag.NewFromAny(string(RequestIDTag), requestID))
80 ev.Tags.Append(tag.NewFromAny(string(StatusTag), string(status)))
81 ev.Tags.Append(tag.NewFromAny(string(RelayTag), sourceRelay))
82
83 // Add optional error tag
84 if errorMsg != "" {
85 ev.Tags.Append(tag.NewFromAny(string(ErrorTag), errorMsg))
86 }
87
88 derr = &DirectoryEventReplicationResponse{
89 Event: ev,
90 Content: content,
91 RequestID: requestID,
92 Status: status,
93 ErrorMsg: errorMsg,
94 SourceRelay: sourceRelay,
95 }
96
97 return
98 }
99
100 // ParseDirectoryEventReplicationResponse parses an event into a
101 // DirectoryEventReplicationResponse structure with validation.
102 func ParseDirectoryEventReplicationResponse(ev *event.E) (derr *DirectoryEventReplicationResponse, err error) {
103 if ev == nil {
104 return nil, errorf.E("event cannot be nil")
105 }
106
107 // Validate event kind
108 if ev.Kind != DirectoryEventReplicationResponseKind.K {
109 return nil, errorf.E("invalid event kind: expected %d, got %d",
110 DirectoryEventReplicationResponseKind.K, ev.Kind)
111 }
112
113 // Parse content
114 var content ReplicationResponseContent
115 if len(ev.Content) > 0 {
116 if err = json.Unmarshal(ev.Content, &content); chk.E(err) {
117 return nil, errorf.E("failed to parse content: %w", err)
118 }
119 }
120
121 // Extract required tags
122 requestIDTag := ev.Tags.GetFirst(RequestIDTag)
123 if requestIDTag == nil {
124 return nil, errorf.E("missing request_id tag")
125 }
126
127 statusTag := ev.Tags.GetFirst(StatusTag)
128 if statusTag == nil {
129 return nil, errorf.E("missing status tag")
130 }
131
132 relayTag := ev.Tags.GetFirst(RelayTag)
133 if relayTag == nil {
134 return nil, errorf.E("missing relay tag")
135 }
136
137 // Validate status
138 status := ReplicationStatus(statusTag.Value())
139 if err = ValidateReplicationStatus(string(status)); chk.E(err) {
140 return
141 }
142
143 // Extract optional error tag
144 var errorMsg string
145 errorTag := ev.Tags.GetFirst(ErrorTag)
146 if errorTag != nil {
147 errorMsg = string(errorTag.Value())
148 }
149
150 derr = &DirectoryEventReplicationResponse{
151 Event: ev,
152 Content: &content,
153 RequestID: string(requestIDTag.Value()),
154 Status: status,
155 ErrorMsg: errorMsg,
156 SourceRelay: string(relayTag.Value()),
157 }
158
159 return
160 }
161
162 // Validate performs comprehensive validation of a DirectoryEventReplicationResponse.
163 func (derr *DirectoryEventReplicationResponse) Validate() (err error) {
164 if derr == nil {
165 return errorf.E("DirectoryEventReplicationResponse cannot be nil")
166 }
167
168 if derr.Event == nil {
169 return errorf.E("event cannot be nil")
170 }
171
172 // Validate event signature
173 if _, err = derr.Event.Verify(); chk.E(err) {
174 return errorf.E("invalid event signature: %w", err)
175 }
176
177 // Validate required fields
178 if derr.RequestID == "" {
179 return errorf.E("request ID is required")
180 }
181
182 if err = ValidateReplicationStatus(string(derr.Status)); chk.E(err) {
183 return
184 }
185
186 if derr.SourceRelay == "" {
187 return errorf.E("source relay is required")
188 }
189
190 if derr.Content == nil {
191 return errorf.E("content cannot be nil")
192 }
193
194 // Validate that content request ID matches tag request ID
195 if derr.Content.RequestID != derr.RequestID {
196 return errorf.E("content request ID does not match tag request ID")
197 }
198
199 // Validate event results
200 for i, result := range derr.Content.Results {
201 if result == nil {
202 return errorf.E("result %d cannot be nil", i)
203 }
204 if result.EventID == "" {
205 return errorf.E("result %d missing event ID", i)
206 }
207 if err = ValidateReplicationStatus(string(result.Status)); chk.E(err) {
208 return errorf.E("result %d has invalid status: %w", i, err)
209 }
210 }
211
212 return nil
213 }
214
215 // NewEventResult creates a new EventResult.
216 func NewEventResult(eventID string, status ReplicationStatus, errorMsg string) *EventResult {
217 return &EventResult{
218 EventID: eventID,
219 Status: status,
220 Error: errorMsg,
221 }
222 }
223
224 // GetRequestID returns the request ID this response corresponds to.
225 func (derr *DirectoryEventReplicationResponse) GetRequestID() string {
226 return derr.RequestID
227 }
228
229 // GetStatus returns the overall replication status.
230 func (derr *DirectoryEventReplicationResponse) GetStatus() ReplicationStatus {
231 return derr.Status
232 }
233
234 // GetErrorMsg returns the error message, if any.
235 func (derr *DirectoryEventReplicationResponse) GetErrorMsg() string {
236 return derr.ErrorMsg
237 }
238
239 // GetSourceRelay returns the relay that sent this response.
240 func (derr *DirectoryEventReplicationResponse) GetSourceRelay() string {
241 return derr.SourceRelay
242 }
243
244 // GetResults returns the list of individual event results.
245 func (derr *DirectoryEventReplicationResponse) GetResults() []*EventResult {
246 if derr.Content == nil {
247 return nil
248 }
249 return derr.Content.Results
250 }
251
252 // GetResultCount returns the number of event results.
253 func (derr *DirectoryEventReplicationResponse) GetResultCount() int {
254 if derr.Content == nil {
255 return 0
256 }
257 return len(derr.Content.Results)
258 }
259
260 // HasResults returns true if the response contains event results.
261 func (derr *DirectoryEventReplicationResponse) HasResults() bool {
262 return derr.GetResultCount() > 0
263 }
264
265 // IsSuccess returns true if the overall replication was successful.
266 func (derr *DirectoryEventReplicationResponse) IsSuccess() bool {
267 return derr.Status == ReplicationStatusSuccess
268 }
269
270 // IsError returns true if the overall replication failed.
271 func (derr *DirectoryEventReplicationResponse) IsError() bool {
272 return derr.Status == ReplicationStatusError
273 }
274
275 // IsPending returns true if the replication is still pending.
276 func (derr *DirectoryEventReplicationResponse) IsPending() bool {
277 return derr.Status == ReplicationStatusPending
278 }
279
280 // GetSuccessfulResults returns all results with success status.
281 func (derr *DirectoryEventReplicationResponse) GetSuccessfulResults() []*EventResult {
282 var results []*EventResult
283 for _, result := range derr.GetResults() {
284 if result.Status == ReplicationStatusSuccess {
285 results = append(results, result)
286 }
287 }
288 return results
289 }
290
291 // GetFailedResults returns all results with error status.
292 func (derr *DirectoryEventReplicationResponse) GetFailedResults() []*EventResult {
293 var results []*EventResult
294 for _, result := range derr.GetResults() {
295 if result.Status == ReplicationStatusError {
296 results = append(results, result)
297 }
298 }
299 return results
300 }
301
302 // GetPendingResults returns all results with pending status.
303 func (derr *DirectoryEventReplicationResponse) GetPendingResults() []*EventResult {
304 var results []*EventResult
305 for _, result := range derr.GetResults() {
306 if result.Status == ReplicationStatusPending {
307 results = append(results, result)
308 }
309 }
310 return results
311 }
312
313 // GetResultByEventID returns the result for a specific event ID, or nil if not found.
314 func (derr *DirectoryEventReplicationResponse) GetResultByEventID(eventID string) *EventResult {
315 for _, result := range derr.GetResults() {
316 if result.EventID == eventID {
317 return result
318 }
319 }
320 return nil
321 }
322
323 // GetSuccessCount returns the number of successfully replicated events.
324 func (derr *DirectoryEventReplicationResponse) GetSuccessCount() int {
325 return len(derr.GetSuccessfulResults())
326 }
327
328 // GetFailureCount returns the number of failed event replications.
329 func (derr *DirectoryEventReplicationResponse) GetFailureCount() int {
330 return len(derr.GetFailedResults())
331 }
332
333 // GetPendingCount returns the number of pending event replications.
334 func (derr *DirectoryEventReplicationResponse) GetPendingCount() int {
335 return len(derr.GetPendingResults())
336 }
337
338 // GetSuccessRate returns the success rate as a percentage (0-100).
339 func (derr *DirectoryEventReplicationResponse) GetSuccessRate() float64 {
340 total := derr.GetResultCount()
341 if total == 0 {
342 return 0
343 }
344 return float64(derr.GetSuccessCount()) / float64(total) * 100
345 }
346
347 // EventResult methods
348
349 // IsSuccess returns true if this event result was successful.
350 func (er *EventResult) IsSuccess() bool {
351 return er.Status == ReplicationStatusSuccess
352 }
353
354 // IsError returns true if this event result failed.
355 func (er *EventResult) IsError() bool {
356 return er.Status == ReplicationStatusError
357 }
358
359 // IsPending returns true if this event result is pending.
360 func (er *EventResult) IsPending() bool {
361 return er.Status == ReplicationStatusPending
362 }
363
364 // HasError returns true if this event result has an error message.
365 func (er *EventResult) HasError() bool {
366 return er.Error != ""
367 }
368