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