relay_identity.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  // RelayIdentityContent represents the JSON content of a Relay Identity
  13  // Announcement event (Kind 39100).
  14  type RelayIdentityContent struct {
  15  	Name        string `json:"name"`
  16  	Description string `json:"description,omitempty"`
  17  	Contact     string `json:"contact,omitempty"`
  18  }
  19  
  20  // RelayIdentityAnnouncement represents a complete Relay Identity Announcement
  21  // event with typed access to its components.
  22  type RelayIdentityAnnouncement struct {
  23  	Event         *event.E
  24  	Content       *RelayIdentityContent
  25  	RelayURL      string
  26  	SigningKey    string
  27  	EncryptionKey string
  28  	Version       string
  29  }
  30  
  31  // NewRelayIdentityAnnouncement creates a new Relay Identity Announcement event.
  32  func NewRelayIdentityAnnouncement(
  33  	pubkey []byte,
  34  	name, description, contact string,
  35  	relayURL, signingKey, encryptionKey, version string,
  36  ) (ria *RelayIdentityAnnouncement, err error) {
  37  
  38  	// Validate required fields
  39  	if len(pubkey) != 32 {
  40  		return nil, errorf.E("pubkey must be 32 bytes")
  41  	}
  42  	if name == "" {
  43  		return nil, errorf.E("name is required")
  44  	}
  45  	if relayURL == "" {
  46  		return nil, errorf.E("relay URL is required")
  47  	}
  48  	if signingKey == "" {
  49  		return nil, errorf.E("signing key is required")
  50  	}
  51  	if encryptionKey == "" {
  52  		return nil, errorf.E("encryption key is required")
  53  	}
  54  	if version == "" {
  55  		version = "1" // Default version
  56  	}
  57  
  58  	// Create content
  59  	content := &RelayIdentityContent{
  60  		Name:        name,
  61  		Description: description,
  62  		Contact:     contact,
  63  	}
  64  
  65  	// Marshal content to JSON
  66  	var contentBytes []byte
  67  	if contentBytes, err = json.Marshal(content); chk.E(err) {
  68  		return
  69  	}
  70  
  71  	// Create base event
  72  	ev := CreateBaseEvent(pubkey, RelayIdentityAnnouncementKind)
  73  	ev.Content = contentBytes
  74  
  75  	// Add required tags
  76  	ev.Tags.Append(tag.NewFromAny(string(DTag), "relay-identity"))
  77  	ev.Tags.Append(tag.NewFromAny(string(RelayTag), relayURL))
  78  	ev.Tags.Append(tag.NewFromAny(string(SigningKeyTag), signingKey))
  79  	ev.Tags.Append(tag.NewFromAny(string(EncryptionKeyTag), encryptionKey))
  80  	ev.Tags.Append(tag.NewFromAny(string(VersionTag), version))
  81  
  82  	ria = &RelayIdentityAnnouncement{
  83  		Event:         ev,
  84  		Content:       content,
  85  		RelayURL:      relayURL,
  86  		SigningKey:    signingKey,
  87  		EncryptionKey: encryptionKey,
  88  		Version:       version,
  89  	}
  90  
  91  	return
  92  }
  93  
  94  // ParseRelayIdentityAnnouncement parses an event into a RelayIdentityAnnouncement
  95  // structure with validation.
  96  func ParseRelayIdentityAnnouncement(ev *event.E) (ria *RelayIdentityAnnouncement, err error) {
  97  	if ev == nil {
  98  		return nil, errorf.E("event cannot be nil")
  99  	}
 100  
 101  	// Validate event kind
 102  	if ev.Kind != RelayIdentityAnnouncementKind.K {
 103  		return nil, errorf.E("invalid event kind: expected %d, got %d",
 104  			RelayIdentityAnnouncementKind.K, ev.Kind)
 105  	}
 106  
 107  	// Parse content
 108  	var content RelayIdentityContent
 109  	if len(ev.Content) > 0 {
 110  		if err = json.Unmarshal(ev.Content, &content); chk.E(err) {
 111  			return nil, errorf.E("failed to parse content: %w", err)
 112  		}
 113  	}
 114  
 115  	// Extract required tags
 116  	dTag := ev.Tags.GetFirst(DTag)
 117  	if dTag == nil || string(dTag.Value()) != "relay-identity" {
 118  		return nil, errorf.E("missing or invalid d tag")
 119  	}
 120  
 121  	relayTag := ev.Tags.GetFirst(RelayTag)
 122  	if relayTag == nil {
 123  		return nil, errorf.E("missing relay tag")
 124  	}
 125  
 126  	signingKeyTag := ev.Tags.GetFirst(SigningKeyTag)
 127  	if signingKeyTag == nil {
 128  		return nil, errorf.E("missing signing_key tag")
 129  	}
 130  
 131  	encryptionKeyTag := ev.Tags.GetFirst(EncryptionKeyTag)
 132  	if encryptionKeyTag == nil {
 133  		return nil, errorf.E("missing encryption_key tag")
 134  	}
 135  
 136  	versionTag := ev.Tags.GetFirst(VersionTag)
 137  	if versionTag == nil {
 138  		return nil, errorf.E("missing version tag")
 139  	}
 140  
 141  	ria = &RelayIdentityAnnouncement{
 142  		Event:         ev,
 143  		Content:       &content,
 144  		RelayURL:      string(relayTag.Value()),
 145  		SigningKey:    string(signingKeyTag.Value()),
 146  		EncryptionKey: string(encryptionKeyTag.Value()),
 147  		Version:       string(versionTag.Value()),
 148  	}
 149  
 150  	return
 151  }
 152  
 153  // Validate performs comprehensive validation of a RelayIdentityAnnouncement.
 154  func (ria *RelayIdentityAnnouncement) Validate() (err error) {
 155  	if ria == nil {
 156  		return errorf.E("RelayIdentityAnnouncement cannot be nil")
 157  	}
 158  
 159  	if ria.Event == nil {
 160  		return errorf.E("event cannot be nil")
 161  	}
 162  
 163  	// Validate event signature
 164  	if _, err = ria.Event.Verify(); chk.E(err) {
 165  		return errorf.E("invalid event signature: %w", err)
 166  	}
 167  
 168  	// Validate required fields
 169  	if ria.Content.Name == "" {
 170  		return errorf.E("name is required")
 171  	}
 172  
 173  	if ria.RelayURL == "" {
 174  		return errorf.E("relay URL is required")
 175  	}
 176  
 177  	if ria.SigningKey == "" {
 178  		return errorf.E("signing key is required")
 179  	}
 180  
 181  	if ria.EncryptionKey == "" {
 182  		return errorf.E("encryption key is required")
 183  	}
 184  
 185  	if ria.Version == "" {
 186  		return errorf.E("version is required")
 187  	}
 188  
 189  	// Validate hex-encoded keys (should be 64 characters for 32-byte keys)
 190  	if len(ria.SigningKey) != 64 {
 191  		return errorf.E("signing key must be 64 hex characters")
 192  	}
 193  
 194  	if len(ria.EncryptionKey) != 64 {
 195  		return errorf.E("encryption key must be 64 hex characters")
 196  	}
 197  
 198  	return nil
 199  }
 200  
 201  // GetRelayURL returns the relay WebSocket URL.
 202  func (ria *RelayIdentityAnnouncement) GetRelayURL() string {
 203  	return ria.RelayURL
 204  }
 205  
 206  // GetSigningKey returns the hex-encoded signing public key.
 207  func (ria *RelayIdentityAnnouncement) GetSigningKey() string {
 208  	return ria.SigningKey
 209  }
 210  
 211  // GetEncryptionKey returns the hex-encoded encryption public key.
 212  func (ria *RelayIdentityAnnouncement) GetEncryptionKey() string {
 213  	return ria.EncryptionKey
 214  }
 215  
 216  // GetVersion returns the protocol version.
 217  func (ria *RelayIdentityAnnouncement) GetVersion() string {
 218  	return ria.Version
 219  }
 220  
 221  // GetName returns the relay name from the content.
 222  func (ria *RelayIdentityAnnouncement) GetName() string {
 223  	if ria.Content == nil {
 224  		return ""
 225  	}
 226  	return ria.Content.Name
 227  }
 228  
 229  // GetDescription returns the relay description from the content.
 230  func (ria *RelayIdentityAnnouncement) GetDescription() string {
 231  	if ria.Content == nil {
 232  		return ""
 233  	}
 234  	return ria.Content.Description
 235  }
 236  
 237  // GetContact returns the relay contact information from the content.
 238  func (ria *RelayIdentityAnnouncement) GetContact() string {
 239  	if ria.Content == nil {
 240  		return ""
 241  	}
 242  	return ria.Content.Contact
 243  }
 244