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