package directory import ( "encoding/json" "lol.mleku.dev/chk" "lol.mleku.dev/errorf" "next.orly.dev/pkg/encoders/event" "next.orly.dev/pkg/encoders/tag" ) // RelayIdentityContent represents the JSON content of a Relay Identity // Announcement event (Kind 39100). type RelayIdentityContent struct { Name string `json:"name"` Description string `json:"description,omitempty"` Contact string `json:"contact,omitempty"` } // RelayIdentityAnnouncement represents a complete Relay Identity Announcement // event with typed access to its components. type RelayIdentityAnnouncement struct { Event *event.E Content *RelayIdentityContent RelayURL string SigningKey string EncryptionKey string Version string NIP11URL string } // NewRelayIdentityAnnouncement creates a new Relay Identity Announcement event. func NewRelayIdentityAnnouncement( pubkey []byte, name, description, contact string, relayURL, signingKey, encryptionKey, version, nip11URL string, ) (ria *RelayIdentityAnnouncement, err error) { // Validate required fields if len(pubkey) != 32 { return nil, errorf.E("pubkey must be 32 bytes") } if name == "" { return nil, errorf.E("name is required") } if relayURL == "" { return nil, errorf.E("relay URL is required") } if signingKey == "" { return nil, errorf.E("signing key is required") } if encryptionKey == "" { return nil, errorf.E("encryption key is required") } if version == "" { version = "1" // Default version } if nip11URL == "" { return nil, errorf.E("NIP-11 URL is required") } // Create content content := &RelayIdentityContent{ Name: name, Description: description, Contact: contact, } // Marshal content to JSON var contentBytes []byte if contentBytes, err = json.Marshal(content); chk.E(err) { return } // Create base event ev := CreateBaseEvent(pubkey, RelayIdentityAnnouncementKind) ev.Content = contentBytes // Add required tags ev.Tags.Append(tag.NewFromAny(string(DTag), "relay-identity")) ev.Tags.Append(tag.NewFromAny(string(RelayTag), relayURL)) ev.Tags.Append(tag.NewFromAny(string(SigningKeyTag), signingKey)) ev.Tags.Append(tag.NewFromAny(string(EncryptionKeyTag), encryptionKey)) ev.Tags.Append(tag.NewFromAny(string(VersionTag), version)) ev.Tags.Append(tag.NewFromAny(string(NIP11URLTag), nip11URL)) ria = &RelayIdentityAnnouncement{ Event: ev, Content: content, RelayURL: relayURL, SigningKey: signingKey, EncryptionKey: encryptionKey, Version: version, NIP11URL: nip11URL, } return } // ParseRelayIdentityAnnouncement parses an event into a RelayIdentityAnnouncement // structure with validation. func ParseRelayIdentityAnnouncement(ev *event.E) (ria *RelayIdentityAnnouncement, err error) { if ev == nil { return nil, errorf.E("event cannot be nil") } // Validate event kind if ev.Kind != RelayIdentityAnnouncementKind.K { return nil, errorf.E("invalid event kind: expected %d, got %d", RelayIdentityAnnouncementKind.K, ev.Kind) } // Parse content var content RelayIdentityContent if len(ev.Content) > 0 { if err = json.Unmarshal(ev.Content, &content); chk.E(err) { return nil, errorf.E("failed to parse content: %w", err) } } // Extract required tags dTag := ev.Tags.GetFirst(DTag) if dTag == nil || string(dTag.Value()) != "relay-identity" { return nil, errorf.E("missing or invalid d tag") } relayTag := ev.Tags.GetFirst(RelayTag) if relayTag == nil { return nil, errorf.E("missing relay tag") } signingKeyTag := ev.Tags.GetFirst(SigningKeyTag) if signingKeyTag == nil { return nil, errorf.E("missing signing_key tag") } encryptionKeyTag := ev.Tags.GetFirst(EncryptionKeyTag) if encryptionKeyTag == nil { return nil, errorf.E("missing encryption_key tag") } versionTag := ev.Tags.GetFirst(VersionTag) if versionTag == nil { return nil, errorf.E("missing version tag") } nip11URLTag := ev.Tags.GetFirst(NIP11URLTag) if nip11URLTag == nil { return nil, errorf.E("missing nip11_url tag") } ria = &RelayIdentityAnnouncement{ Event: ev, Content: &content, RelayURL: string(relayTag.Value()), SigningKey: string(signingKeyTag.Value()), EncryptionKey: string(encryptionKeyTag.Value()), Version: string(versionTag.Value()), NIP11URL: string(nip11URLTag.Value()), } return } // Validate performs comprehensive validation of a RelayIdentityAnnouncement. func (ria *RelayIdentityAnnouncement) Validate() (err error) { if ria == nil { return errorf.E("RelayIdentityAnnouncement cannot be nil") } if ria.Event == nil { return errorf.E("event cannot be nil") } // Validate event signature if _, err = ria.Event.Verify(); chk.E(err) { return errorf.E("invalid event signature: %w", err) } // Validate required fields if ria.Content.Name == "" { return errorf.E("name is required") } if ria.RelayURL == "" { return errorf.E("relay URL is required") } if ria.SigningKey == "" { return errorf.E("signing key is required") } if ria.EncryptionKey == "" { return errorf.E("encryption key is required") } if ria.Version == "" { return errorf.E("version is required") } if ria.NIP11URL == "" { return errorf.E("NIP-11 URL is required") } // Validate hex-encoded keys (should be 64 characters for 32-byte keys) if len(ria.SigningKey) != 64 { return errorf.E("signing key must be 64 hex characters") } if len(ria.EncryptionKey) != 64 { return errorf.E("encryption key must be 64 hex characters") } return nil } // GetRelayURL returns the relay WebSocket URL. func (ria *RelayIdentityAnnouncement) GetRelayURL() string { return ria.RelayURL } // GetSigningKey returns the hex-encoded signing public key. func (ria *RelayIdentityAnnouncement) GetSigningKey() string { return ria.SigningKey } // GetEncryptionKey returns the hex-encoded encryption public key. func (ria *RelayIdentityAnnouncement) GetEncryptionKey() string { return ria.EncryptionKey } // GetVersion returns the protocol version. func (ria *RelayIdentityAnnouncement) GetVersion() string { return ria.Version } // GetNIP11URL returns the NIP-11 information document URL. func (ria *RelayIdentityAnnouncement) GetNIP11URL() string { return ria.NIP11URL } // GetName returns the relay name from the content. func (ria *RelayIdentityAnnouncement) GetName() string { if ria.Content == nil { return "" } return ria.Content.Name } // GetDescription returns the relay description from the content. func (ria *RelayIdentityAnnouncement) GetDescription() string { if ria.Content == nil { return "" } return ria.Content.Description } // GetContact returns the relay contact information from the content. func (ria *RelayIdentityAnnouncement) GetContact() string { if ria.Content == nil { return "" } return ria.Content.Contact }