parser.go raw
1 package find
2
3 import (
4 "encoding/json"
5 "fmt"
6 "strconv"
7 "strings"
8 "time"
9
10 "next.orly.dev/pkg/nostr/encoders/event"
11 "next.orly.dev/pkg/nostr/encoders/hex"
12 "next.orly.dev/pkg/nostr/encoders/tag"
13 )
14
15 // Tag binary encoding constants (matching the nostr library)
16 const (
17 binaryEncodedLen = 33 // 32 bytes hash + null terminator
18 hexEncodedLen = 64 // 64 hex characters for 32 bytes
19 hashLen = 32
20 )
21
22 // isBinaryEncoded checks if a value is stored in the nostr library's binary-optimized format
23 func isBinaryEncoded(val []byte) bool {
24 return len(val) == binaryEncodedLen && val[hashLen] == 0
25 }
26
27 // normalizePubkeyHex ensures a pubkey is in lowercase hex format.
28 // Handles binary-encoded values (33 bytes) and uppercase hex strings.
29 func normalizePubkeyHex(val []byte) string {
30 if isBinaryEncoded(val) {
31 return hex.Enc(val[:hashLen])
32 }
33 if len(val) == hexEncodedLen {
34 return strings.ToLower(string(val))
35 }
36 return strings.ToLower(string(val))
37 }
38
39 // extractPTagValue extracts a pubkey from a p-tag, handling binary encoding.
40 // Returns lowercase hex string.
41 func extractPTagValue(t *tag.T) string {
42 if t == nil || len(t.T) < 2 {
43 return ""
44 }
45 hexVal := t.ValueHex()
46 if len(hexVal) == 0 {
47 return ""
48 }
49 return strings.ToLower(string(hexVal))
50 }
51
52 // getTagValue retrieves the value of the first tag with the given key
53 func getTagValue(ev *event.E, key string) string {
54 t := ev.Tags.GetFirst([]byte(key))
55 if t == nil {
56 return ""
57 }
58 return string(t.Value())
59 }
60
61 // getAllTags retrieves all tags with the given key
62 func getAllTags(ev *event.E, key string) []*tag.T {
63 return ev.Tags.GetAll([]byte(key))
64 }
65
66 // ParseRegistrationProposal parses a kind 30100 event into a RegistrationProposal
67 func ParseRegistrationProposal(ev *event.E) (*RegistrationProposal, error) {
68 if uint16(ev.Kind) != KindRegistrationProposal {
69 return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindRegistrationProposal, ev.Kind)
70 }
71
72 name := getTagValue(ev, "d")
73 if name == "" {
74 return nil, fmt.Errorf("missing 'd' tag (name)")
75 }
76
77 action := getTagValue(ev, "action")
78 if action == "" {
79 return nil, fmt.Errorf("missing 'action' tag")
80 }
81
82 expirationStr := getTagValue(ev, "expiration")
83 var expiration time.Time
84 if expirationStr != "" {
85 expirationUnix, err := strconv.ParseInt(expirationStr, 10, 64)
86 if err != nil {
87 return nil, fmt.Errorf("invalid expiration timestamp: %w", err)
88 }
89 expiration = time.Unix(expirationUnix, 0)
90 }
91
92 proposal := &RegistrationProposal{
93 Event: ev,
94 Name: name,
95 Action: action,
96 PrevOwner: getTagValue(ev, "prev_owner"),
97 PrevSig: getTagValue(ev, "prev_sig"),
98 Expiration: expiration,
99 }
100
101 return proposal, nil
102 }
103
104 // ParseAttestation parses a kind 20100 event into an Attestation
105 func ParseAttestation(ev *event.E) (*Attestation, error) {
106 if uint16(ev.Kind) != KindAttestation {
107 return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindAttestation, ev.Kind)
108 }
109
110 proposalID := getTagValue(ev, "e")
111 if proposalID == "" {
112 return nil, fmt.Errorf("missing 'e' tag (proposal ID)")
113 }
114
115 decision := getTagValue(ev, "decision")
116 if decision == "" {
117 return nil, fmt.Errorf("missing 'decision' tag")
118 }
119
120 weightStr := getTagValue(ev, "weight")
121 weight := 100 // default weight
122 if weightStr != "" {
123 w, err := strconv.Atoi(weightStr)
124 if err != nil {
125 return nil, fmt.Errorf("invalid weight value: %w", err)
126 }
127 weight = w
128 }
129
130 expirationStr := getTagValue(ev, "expiration")
131 var expiration time.Time
132 if expirationStr != "" {
133 expirationUnix, err := strconv.ParseInt(expirationStr, 10, 64)
134 if err != nil {
135 return nil, fmt.Errorf("invalid expiration timestamp: %w", err)
136 }
137 expiration = time.Unix(expirationUnix, 0)
138 }
139
140 attestation := &Attestation{
141 Event: ev,
142 ProposalID: proposalID,
143 Decision: decision,
144 Weight: weight,
145 Reason: getTagValue(ev, "reason"),
146 ServiceURL: getTagValue(ev, "service"),
147 Expiration: expiration,
148 }
149
150 return attestation, nil
151 }
152
153 // ParseTrustGraph parses a kind 30101 event into a TrustGraphEvent
154 func ParseTrustGraph(ev *event.E) (*TrustGraphEvent, error) {
155 if uint16(ev.Kind) != KindTrustGraph {
156 return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindTrustGraph, ev.Kind)
157 }
158
159 expirationStr := getTagValue(ev, "expiration")
160 var expiration time.Time
161 if expirationStr != "" {
162 expirationUnix, err := strconv.ParseInt(expirationStr, 10, 64)
163 if err != nil {
164 return nil, fmt.Errorf("invalid expiration timestamp: %w", err)
165 }
166 expiration = time.Unix(expirationUnix, 0)
167 }
168
169 // Parse p tags (trust entries)
170 // Use extractPTagValue to handle binary-encoded pubkeys
171 var entries []TrustEntry
172 pTags := getAllTags(ev, "p")
173 for _, t := range pTags {
174 if len(t.T) < 2 {
175 continue // Skip malformed tags
176 }
177
178 // Use extractPTagValue to handle binary encoding and normalize to lowercase hex
179 pubkey := extractPTagValue(t)
180 if pubkey == "" {
181 continue // Skip invalid p-tags
182 }
183
184 serviceURL := ""
185 trustScore := 0.5 // default
186
187 if len(t.T) > 2 {
188 serviceURL = string(t.T[2])
189 }
190
191 if len(t.T) > 3 {
192 score, err := strconv.ParseFloat(string(t.T[3]), 64)
193 if err == nil {
194 trustScore = score
195 }
196 }
197
198 entries = append(entries, TrustEntry{
199 Pubkey: pubkey,
200 ServiceURL: serviceURL,
201 TrustScore: trustScore,
202 })
203 }
204
205 return &TrustGraphEvent{
206 Event: ev,
207 Entries: entries,
208 Expiration: expiration,
209 }, nil
210 }
211
212 // ParseNameState parses a kind 30102 event into a NameState
213 func ParseNameState(ev *event.E) (*NameState, error) {
214 if uint16(ev.Kind) != KindNameState {
215 return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindNameState, ev.Kind)
216 }
217
218 name := getTagValue(ev, "d")
219 if name == "" {
220 return nil, fmt.Errorf("missing 'd' tag (name)")
221 }
222
223 owner := getTagValue(ev, "owner")
224 if owner == "" {
225 return nil, fmt.Errorf("missing 'owner' tag")
226 }
227
228 registeredAtStr := getTagValue(ev, "registered_at")
229 if registeredAtStr == "" {
230 return nil, fmt.Errorf("missing 'registered_at' tag")
231 }
232 registeredAtUnix, err := strconv.ParseInt(registeredAtStr, 10, 64)
233 if err != nil {
234 return nil, fmt.Errorf("invalid registered_at timestamp: %w", err)
235 }
236 registeredAt := time.Unix(registeredAtUnix, 0)
237
238 attestationsStr := getTagValue(ev, "attestations")
239 attestations := 0
240 if attestationsStr != "" {
241 a, err := strconv.Atoi(attestationsStr)
242 if err == nil {
243 attestations = a
244 }
245 }
246
247 confidenceStr := getTagValue(ev, "confidence")
248 confidence := 0.0
249 if confidenceStr != "" {
250 c, err := strconv.ParseFloat(confidenceStr, 64)
251 if err == nil {
252 confidence = c
253 }
254 }
255
256 expirationStr := getTagValue(ev, "expiration")
257 var expiration time.Time
258 if expirationStr != "" {
259 expirationUnix, err := strconv.ParseInt(expirationStr, 10, 64)
260 if err != nil {
261 return nil, fmt.Errorf("invalid expiration timestamp: %w", err)
262 }
263 expiration = time.Unix(expirationUnix, 0)
264 }
265
266 return &NameState{
267 Event: ev,
268 Name: name,
269 Owner: owner,
270 RegisteredAt: registeredAt,
271 ProposalID: getTagValue(ev, "proposal"),
272 Attestations: attestations,
273 Confidence: confidence,
274 Expiration: expiration,
275 }, nil
276 }
277
278 // ParseNameRecord parses a kind 30103 event into a NameRecord
279 func ParseNameRecord(ev *event.E) (*NameRecord, error) {
280 if uint16(ev.Kind) != KindNameRecords {
281 return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindNameRecords, ev.Kind)
282 }
283
284 name := getTagValue(ev, "name")
285 if name == "" {
286 return nil, fmt.Errorf("missing 'name' tag")
287 }
288
289 recordType := getTagValue(ev, "type")
290 if recordType == "" {
291 return nil, fmt.Errorf("missing 'type' tag")
292 }
293
294 value := getTagValue(ev, "value")
295 if value == "" {
296 return nil, fmt.Errorf("missing 'value' tag")
297 }
298
299 ttlStr := getTagValue(ev, "ttl")
300 ttl := 3600 // default TTL
301 if ttlStr != "" {
302 t, err := strconv.Atoi(ttlStr)
303 if err == nil {
304 ttl = t
305 }
306 }
307
308 priorityStr := getTagValue(ev, "priority")
309 priority := 0
310 if priorityStr != "" {
311 p, err := strconv.Atoi(priorityStr)
312 if err == nil {
313 priority = p
314 }
315 }
316
317 weightStr := getTagValue(ev, "weight")
318 weight := 0
319 if weightStr != "" {
320 w, err := strconv.Atoi(weightStr)
321 if err == nil {
322 weight = w
323 }
324 }
325
326 portStr := getTagValue(ev, "port")
327 port := 0
328 if portStr != "" {
329 p, err := strconv.Atoi(portStr)
330 if err == nil {
331 port = p
332 }
333 }
334
335 return &NameRecord{
336 Event: ev,
337 Name: name,
338 Type: recordType,
339 Value: value,
340 TTL: ttl,
341 Priority: priority,
342 Weight: weight,
343 Port: port,
344 }, nil
345 }
346
347 // ParseCertificate parses a kind 30104 event into a Certificate
348 func ParseCertificate(ev *event.E) (*Certificate, error) {
349 if uint16(ev.Kind) != KindCertificate {
350 return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindCertificate, ev.Kind)
351 }
352
353 name := getTagValue(ev, "name")
354 if name == "" {
355 return nil, fmt.Errorf("missing 'name' tag")
356 }
357
358 certPubkey := getTagValue(ev, "cert_pubkey")
359 if certPubkey == "" {
360 return nil, fmt.Errorf("missing 'cert_pubkey' tag")
361 }
362
363 validFromStr := getTagValue(ev, "valid_from")
364 if validFromStr == "" {
365 return nil, fmt.Errorf("missing 'valid_from' tag")
366 }
367 validFromUnix, err := strconv.ParseInt(validFromStr, 10, 64)
368 if err != nil {
369 return nil, fmt.Errorf("invalid valid_from timestamp: %w", err)
370 }
371 validFrom := time.Unix(validFromUnix, 0)
372
373 validUntilStr := getTagValue(ev, "valid_until")
374 if validUntilStr == "" {
375 return nil, fmt.Errorf("missing 'valid_until' tag")
376 }
377 validUntilUnix, err := strconv.ParseInt(validUntilStr, 10, 64)
378 if err != nil {
379 return nil, fmt.Errorf("invalid valid_until timestamp: %w", err)
380 }
381 validUntil := time.Unix(validUntilUnix, 0)
382
383 // Parse witness tags
384 // Note: "witness" is a custom tag key (not "p"), so it doesn't have binary encoding,
385 // but we normalize the pubkey to lowercase for consistency
386 var witnesses []WitnessSignature
387 witnessTags := getAllTags(ev, "witness")
388 for _, t := range witnessTags {
389 if len(t.T) < 3 {
390 continue // Skip malformed tags
391 }
392
393 witnesses = append(witnesses, WitnessSignature{
394 Pubkey: normalizePubkeyHex(t.T[1]), // Normalize to lowercase
395 Signature: string(t.T[2]),
396 })
397 }
398
399 // Parse content JSON
400 algorithm := "secp256k1-schnorr"
401 usage := "tls-replacement"
402 if len(ev.Content) > 0 {
403 var metadata map[string]interface{}
404 if err := json.Unmarshal(ev.Content, &metadata); err == nil {
405 if alg, ok := metadata["algorithm"].(string); ok {
406 algorithm = alg
407 }
408 if u, ok := metadata["usage"].(string); ok {
409 usage = u
410 }
411 }
412 }
413
414 return &Certificate{
415 Event: ev,
416 Name: name,
417 CertPubkey: certPubkey,
418 ValidFrom: validFrom,
419 ValidUntil: validUntil,
420 Challenge: getTagValue(ev, "challenge"),
421 ChallengeProof: getTagValue(ev, "challenge_proof"),
422 Witnesses: witnesses,
423 Algorithm: algorithm,
424 Usage: usage,
425 }, nil
426 }
427
428 // ParseWitnessService parses a kind 30105 event into a WitnessService
429 func ParseWitnessService(ev *event.E) (*WitnessService, error) {
430 if uint16(ev.Kind) != KindWitnessService {
431 return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindWitnessService, ev.Kind)
432 }
433
434 endpoint := getTagValue(ev, "endpoint")
435 if endpoint == "" {
436 return nil, fmt.Errorf("missing 'endpoint' tag")
437 }
438
439 // Parse challenge tags
440 var challenges []string
441 challengeTags := getAllTags(ev, "challenges")
442 for _, t := range challengeTags {
443 if len(t.T) >= 2 {
444 challenges = append(challenges, string(t.T[1]))
445 }
446 }
447
448 maxValidityStr := getTagValue(ev, "max_validity")
449 maxValidity := 0
450 if maxValidityStr != "" {
451 mv, err := strconv.Atoi(maxValidityStr)
452 if err == nil {
453 maxValidity = mv
454 }
455 }
456
457 feeStr := getTagValue(ev, "fee")
458 fee := 0
459 if feeStr != "" {
460 f, err := strconv.Atoi(feeStr)
461 if err == nil {
462 fee = f
463 }
464 }
465
466 expirationStr := getTagValue(ev, "expiration")
467 var expiration time.Time
468 if expirationStr != "" {
469 expirationUnix, err := strconv.ParseInt(expirationStr, 10, 64)
470 if err != nil {
471 return nil, fmt.Errorf("invalid expiration timestamp: %w", err)
472 }
473 expiration = time.Unix(expirationUnix, 0)
474 }
475
476 // Parse content JSON
477 description := ""
478 contact := ""
479 if len(ev.Content) > 0 {
480 var metadata map[string]interface{}
481 if err := json.Unmarshal(ev.Content, &metadata); err == nil {
482 if desc, ok := metadata["description"].(string); ok {
483 description = desc
484 }
485 if cont, ok := metadata["contact"].(string); ok {
486 contact = cont
487 }
488 }
489 }
490
491 return &WitnessService{
492 Event: ev,
493 Endpoint: endpoint,
494 Challenges: challenges,
495 MaxValidity: maxValidity,
496 Fee: fee,
497 ReputationID: getTagValue(ev, "reputation"),
498 Description: description,
499 Contact: contact,
500 Expiration: expiration,
501 }, nil
502 }
503