Files
next.orly.dev/pkg/protocol/directory/relay_identity.go
2025-11-23 08:15:06 +00:00

244 lines
6.2 KiB
Go

package directory
import (
"encoding/json"
"lol.mleku.dev/chk"
"lol.mleku.dev/errorf"
"git.mleku.dev/mleku/nostr/encoders/event"
"git.mleku.dev/mleku/nostr/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
}
// NewRelayIdentityAnnouncement creates a new Relay Identity Announcement event.
func NewRelayIdentityAnnouncement(
pubkey []byte,
name, description, contact string,
relayURL, signingKey, encryptionKey, version 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
}
// 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))
ria = &RelayIdentityAnnouncement{
Event: ev,
Content: content,
RelayURL: relayURL,
SigningKey: signingKey,
EncryptionKey: encryptionKey,
Version: version,
}
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")
}
ria = &RelayIdentityAnnouncement{
Event: ev,
Content: &content,
RelayURL: string(relayTag.Value()),
SigningKey: string(signingKeyTag.Value()),
EncryptionKey: string(encryptionKeyTag.Value()),
Version: string(versionTag.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")
}
// 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
}
// 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
}