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