jsbridge.go raw

   1  //go:build js && wasm
   2  
   3  package wasmdb
   4  
   5  import (
   6  	"bytes"
   7  	"context"
   8  	"encoding/json"
   9  	"fmt"
  10  	"syscall/js"
  11  
  12  	"next.orly.dev/pkg/nostr/encoders/event"
  13  	"next.orly.dev/pkg/nostr/encoders/filter"
  14  	"next.orly.dev/pkg/nostr/encoders/hex"
  15  	"next.orly.dev/pkg/nostr/encoders/kind"
  16  	"next.orly.dev/pkg/nostr/encoders/tag"
  17  	"next.orly.dev/pkg/nostr/encoders/timestamp"
  18  )
  19  
  20  // JSBridge holds the database instance for JavaScript access
  21  var jsBridge *JSBridge
  22  
  23  // JSBridge wraps the WasmDB instance for JavaScript interop
  24  // Exposes a relay protocol interface (NIP-01) rather than direct database access
  25  type JSBridge struct {
  26  	db     *W
  27  	ctx    context.Context
  28  	cancel context.CancelFunc
  29  }
  30  
  31  // RegisterJSBridge exposes the relay protocol API to JavaScript
  32  func RegisterJSBridge(db *W, ctx context.Context, cancel context.CancelFunc) {
  33  	jsBridge = &JSBridge{
  34  		db:     db,
  35  		ctx:    ctx,
  36  		cancel: cancel,
  37  	}
  38  
  39  	// Create the wasmdb global object with relay protocol interface
  40  	wasmdbObj := map[string]interface{}{
  41  		// Lifecycle
  42  		"isReady": js.FuncOf(jsBridge.jsIsReady),
  43  		"close":   js.FuncOf(jsBridge.jsClose),
  44  		"wipe":    js.FuncOf(jsBridge.jsWipe),
  45  
  46  		// Relay Protocol (NIP-01)
  47  		// This is the main entry point - handles EVENT, REQ, CLOSE messages
  48  		"handleMessage": js.FuncOf(jsBridge.jsHandleMessage),
  49  
  50  		// Graph Query Extensions
  51  		"queryGraph": js.FuncOf(jsBridge.jsQueryGraph),
  52  
  53  		// Marker Extensions (key-value storage via relay protocol)
  54  		// ["MARKER", "set", key, value] -> ["OK", key, true]
  55  		// ["MARKER", "get", key] -> ["MARKER", key, value]
  56  		// ["MARKER", "delete", key] -> ["OK", key, true]
  57  		// These are also handled via handleMessage
  58  	}
  59  
  60  	js.Global().Set("wasmdb", wasmdbObj)
  61  }
  62  
  63  // jsIsReady returns true if the database is ready
  64  func (b *JSBridge) jsIsReady(this js.Value, args []js.Value) interface{} {
  65  	select {
  66  	case <-b.db.Ready():
  67  		return true
  68  	default:
  69  		return false
  70  	}
  71  }
  72  
  73  // jsClose closes the database
  74  func (b *JSBridge) jsClose(this js.Value, args []js.Value) interface{} {
  75  	return promiseWrapper(func() (interface{}, error) {
  76  		err := b.db.Close()
  77  		return nil, err
  78  	})
  79  }
  80  
  81  // jsWipe wipes all data from the database
  82  func (b *JSBridge) jsWipe(this js.Value, args []js.Value) interface{} {
  83  	return promiseWrapper(func() (interface{}, error) {
  84  		err := b.db.Wipe()
  85  		return nil, err
  86  	})
  87  }
  88  
  89  // jsHandleMessage handles NIP-01 relay protocol messages
  90  // Input: JSON string representing a relay message array
  91  //
  92  //	["EVENT", <event>] - Submit an event
  93  //	["REQ", <sub_id>, <filter>...] - Request events
  94  //	["CLOSE", <sub_id>] - Close a subscription
  95  //	["MARKER", "set"|"get"|"delete", key, value?] - Marker operations
  96  //
  97  // Output: Promise<string[]> - Array of JSON response messages
  98  func (b *JSBridge) jsHandleMessage(this js.Value, args []js.Value) interface{} {
  99  	if len(args) < 1 {
 100  		return rejectPromise("handleMessage requires message JSON argument")
 101  	}
 102  
 103  	messageJSON := args[0].String()
 104  
 105  	return promiseWrapper(func() (interface{}, error) {
 106  		// Parse the message array
 107  		var message []json.RawMessage
 108  		if err := json.Unmarshal([]byte(messageJSON), &message); err != nil {
 109  			return nil, fmt.Errorf("invalid message format: %w", err)
 110  		}
 111  
 112  		if len(message) < 1 {
 113  			return nil, fmt.Errorf("empty message")
 114  		}
 115  
 116  		// Get message type
 117  		var msgType string
 118  		if err := json.Unmarshal(message[0], &msgType); err != nil {
 119  			return nil, fmt.Errorf("invalid message type: %w", err)
 120  		}
 121  
 122  		switch msgType {
 123  		case "EVENT":
 124  			return b.handleEvent(message)
 125  		case "REQ":
 126  			return b.handleReq(message)
 127  		case "CLOSE":
 128  			return b.handleClose(message)
 129  		case "MARKER":
 130  			return b.handleMarker(message)
 131  		default:
 132  			return nil, fmt.Errorf("unknown message type: %s", msgType)
 133  		}
 134  	})
 135  }
 136  
 137  // handleEvent processes an EVENT message
 138  // ["EVENT", <event>] -> ["OK", <id>, true/false, "message"]
 139  func (b *JSBridge) handleEvent(message []json.RawMessage) (interface{}, error) {
 140  	if len(message) < 2 {
 141  		return []interface{}{makeOK("", false, "missing event")}, nil
 142  	}
 143  
 144  	// Parse the event
 145  	ev, err := parseEventFromRawJSON(message[1])
 146  	if err != nil {
 147  		return []interface{}{makeOK("", false, fmt.Sprintf("invalid event: %s", err))}, nil
 148  	}
 149  
 150  	eventIDHex := hex.Enc(ev.ID)
 151  
 152  	// Save to database
 153  	replaced, err := b.db.SaveEvent(b.ctx, ev)
 154  	if err != nil {
 155  		return []interface{}{makeOK(eventIDHex, false, err.Error())}, nil
 156  	}
 157  
 158  	var msg string
 159  	if replaced {
 160  		msg = "replaced"
 161  	} else {
 162  		msg = "saved"
 163  	}
 164  
 165  	return []interface{}{makeOK(eventIDHex, true, msg)}, nil
 166  }
 167  
 168  // handleReq processes a REQ message
 169  // ["REQ", <sub_id>, <filter>...] -> ["EVENT", <sub_id>, <event>]..., ["EOSE", <sub_id>]
 170  func (b *JSBridge) handleReq(message []json.RawMessage) (interface{}, error) {
 171  	if len(message) < 2 {
 172  		return nil, fmt.Errorf("REQ requires subscription ID")
 173  	}
 174  
 175  	// Get subscription ID
 176  	var subID string
 177  	if err := json.Unmarshal(message[1], &subID); err != nil {
 178  		return nil, fmt.Errorf("invalid subscription ID: %w", err)
 179  	}
 180  
 181  	// Parse filters (can have multiple)
 182  	var allEvents []*event.E
 183  	for i := 2; i < len(message); i++ {
 184  		f, err := parseFilterFromRawJSON(message[i])
 185  		if err != nil {
 186  			continue
 187  		}
 188  
 189  		events, err := b.db.QueryEvents(b.ctx, f)
 190  		if err != nil {
 191  			continue
 192  		}
 193  
 194  		allEvents = append(allEvents, events...)
 195  	}
 196  
 197  	// Build response messages
 198  	responses := make([]interface{}, 0, len(allEvents)+1)
 199  
 200  	// Add EVENT messages
 201  	for _, ev := range allEvents {
 202  		eventJSON, err := eventToJSON(ev)
 203  		if err != nil {
 204  			continue
 205  		}
 206  		responses = append(responses, makeEvent(subID, string(eventJSON)))
 207  	}
 208  
 209  	// Add EOSE
 210  	responses = append(responses, makeEOSE(subID))
 211  
 212  	return responses, nil
 213  }
 214  
 215  // handleClose processes a CLOSE message
 216  // ["CLOSE", <sub_id>] -> (no response for local relay)
 217  func (b *JSBridge) handleClose(message []json.RawMessage) (interface{}, error) {
 218  	// For the local relay, subscriptions are stateless (single query/response)
 219  	// CLOSE is a no-op but we acknowledge it
 220  	return []interface{}{}, nil
 221  }
 222  
 223  // handleMarker processes MARKER extension messages
 224  // ["MARKER", "set", key, value] -> ["OK", key, true]
 225  // ["MARKER", "get", key] -> ["MARKER", key, value] or ["MARKER", key, null]
 226  // ["MARKER", "delete", key] -> ["OK", key, true]
 227  func (b *JSBridge) handleMarker(message []json.RawMessage) (interface{}, error) {
 228  	if len(message) < 3 {
 229  		return nil, fmt.Errorf("MARKER requires operation and key")
 230  	}
 231  
 232  	var operation string
 233  	if err := json.Unmarshal(message[1], &operation); err != nil {
 234  		return nil, fmt.Errorf("invalid marker operation: %w", err)
 235  	}
 236  
 237  	var key string
 238  	if err := json.Unmarshal(message[2], &key); err != nil {
 239  		return nil, fmt.Errorf("invalid marker key: %w", err)
 240  	}
 241  
 242  	switch operation {
 243  	case "set":
 244  		if len(message) < 4 {
 245  			return nil, fmt.Errorf("MARKER set requires value")
 246  		}
 247  		var value string
 248  		if err := json.Unmarshal(message[3], &value); err != nil {
 249  			return nil, fmt.Errorf("invalid marker value: %w", err)
 250  		}
 251  		if err := b.db.SetMarker(key, []byte(value)); err != nil {
 252  			return []interface{}{makeMarkerOK(key, false, err.Error())}, nil
 253  		}
 254  		return []interface{}{makeMarkerOK(key, true, "")}, nil
 255  
 256  	case "get":
 257  		value, err := b.db.GetMarker(key)
 258  		if err != nil || value == nil {
 259  			return []interface{}{makeMarkerResult(key, nil)}, nil
 260  		}
 261  		valueStr := string(value)
 262  		return []interface{}{makeMarkerResult(key, &valueStr)}, nil
 263  
 264  	case "delete":
 265  		if err := b.db.DeleteMarker(key); err != nil {
 266  			return []interface{}{makeMarkerOK(key, false, err.Error())}, nil
 267  		}
 268  		return []interface{}{makeMarkerOK(key, true, "")}, nil
 269  
 270  	case "has":
 271  		has := b.db.HasMarker(key)
 272  		return []interface{}{makeMarkerHas(key, has)}, nil
 273  
 274  	default:
 275  		return nil, fmt.Errorf("unknown marker operation: %s", operation)
 276  	}
 277  }
 278  
 279  // jsQueryGraph handles graph query extensions
 280  // Args: [queryJSON: string] - JSON-encoded graph query
 281  // Returns: Promise<string> - JSON-encoded graph result
 282  func (b *JSBridge) jsQueryGraph(this js.Value, args []js.Value) interface{} {
 283  	if len(args) < 1 {
 284  		return rejectPromise("queryGraph requires query JSON argument")
 285  	}
 286  
 287  	queryJSON := args[0].String()
 288  
 289  	return promiseWrapper(func() (interface{}, error) {
 290  		var query struct {
 291  			Type   string `json:"type"`
 292  			Pubkey string `json:"pubkey"`
 293  			Depth  int    `json:"depth,omitempty"`
 294  			Limit  int    `json:"limit,omitempty"`
 295  		}
 296  
 297  		if err := json.Unmarshal([]byte(queryJSON), &query); err != nil {
 298  			return nil, fmt.Errorf("invalid graph query: %w", err)
 299  		}
 300  
 301  		// Set defaults
 302  		if query.Depth == 0 {
 303  			query.Depth = 1
 304  		}
 305  		if query.Limit == 0 {
 306  			query.Limit = 1000
 307  		}
 308  
 309  		switch query.Type {
 310  		case "follows":
 311  			return b.queryFollows(query.Pubkey, query.Depth, query.Limit)
 312  		case "followers":
 313  			return b.queryFollowers(query.Pubkey, query.Limit)
 314  		case "mutes":
 315  			return b.queryMutes(query.Pubkey)
 316  		default:
 317  			return nil, fmt.Errorf("unknown graph query type: %s", query.Type)
 318  		}
 319  	})
 320  }
 321  
 322  // queryFollows returns who a pubkey follows
 323  func (b *JSBridge) queryFollows(pubkeyHex string, depth, limit int) (interface{}, error) {
 324  	// Query kind 3 (contact list) for the pubkey
 325  	f := &filter.F{
 326  		Kinds: kind.NewWithCap(1),
 327  	}
 328  	f.Kinds.K = append(f.Kinds.K, kind.New(3))
 329  	f.Authors = tag.NewWithCap(1)
 330  	f.Authors.T = append(f.Authors.T, []byte(pubkeyHex))
 331  	one := uint(1)
 332  	f.Limit = &one
 333  
 334  	events, err := b.db.QueryEvents(b.ctx, f)
 335  	if err != nil {
 336  		return nil, err
 337  	}
 338  
 339  	var follows []string
 340  	if len(events) > 0 && events[0].Tags != nil {
 341  		for _, t := range *events[0].Tags {
 342  			if t != nil && len(t.T) >= 2 && string(t.T[0]) == "p" {
 343  				follows = append(follows, string(t.T[1]))
 344  			}
 345  		}
 346  	}
 347  
 348  	result := map[string]interface{}{
 349  		"nodes": follows,
 350  	}
 351  	jsonBytes, err := json.Marshal(result)
 352  	if err != nil {
 353  		return nil, err
 354  	}
 355  	return string(jsonBytes), nil
 356  }
 357  
 358  // queryFollowers returns who follows a pubkey
 359  func (b *JSBridge) queryFollowers(pubkeyHex string, limit int) (interface{}, error) {
 360  	// Query kind 3 events that tag this pubkey
 361  	f := &filter.F{
 362  		Kinds: kind.NewWithCap(1),
 363  		Tags:  tag.NewSWithCap(1),
 364  	}
 365  	f.Kinds.K = append(f.Kinds.K, kind.New(3))
 366  
 367  	// Add #p tag filter
 368  	pTag := tag.NewWithCap(2)
 369  	pTag.T = append(pTag.T, []byte("p"))
 370  	pTag.T = append(pTag.T, []byte(pubkeyHex))
 371  	f.Tags.Append(pTag)
 372  
 373  	lim := uint(limit)
 374  	f.Limit = &lim
 375  
 376  	events, err := b.db.QueryEvents(b.ctx, f)
 377  	if err != nil {
 378  		return nil, err
 379  	}
 380  
 381  	var followers []string
 382  	for _, ev := range events {
 383  		followers = append(followers, hex.Enc(ev.Pubkey))
 384  	}
 385  
 386  	result := map[string]interface{}{
 387  		"nodes": followers,
 388  	}
 389  	jsonBytes, err := json.Marshal(result)
 390  	if err != nil {
 391  		return nil, err
 392  	}
 393  	return string(jsonBytes), nil
 394  }
 395  
 396  // queryMutes returns who a pubkey has muted
 397  func (b *JSBridge) queryMutes(pubkeyHex string) (interface{}, error) {
 398  	// Query kind 10000 (mute list) for the pubkey
 399  	f := &filter.F{
 400  		Kinds: kind.NewWithCap(1),
 401  	}
 402  	f.Kinds.K = append(f.Kinds.K, kind.New(10000))
 403  	f.Authors = tag.NewWithCap(1)
 404  	f.Authors.T = append(f.Authors.T, []byte(pubkeyHex))
 405  	one := uint(1)
 406  	f.Limit = &one
 407  
 408  	events, err := b.db.QueryEvents(b.ctx, f)
 409  	if err != nil {
 410  		return nil, err
 411  	}
 412  
 413  	var mutes []string
 414  	if len(events) > 0 && events[0].Tags != nil {
 415  		for _, t := range *events[0].Tags {
 416  			if t != nil && len(t.T) >= 2 && string(t.T[0]) == "p" {
 417  				mutes = append(mutes, string(t.T[1]))
 418  			}
 419  		}
 420  	}
 421  
 422  	result := map[string]interface{}{
 423  		"nodes": mutes,
 424  	}
 425  	jsonBytes, err := json.Marshal(result)
 426  	if err != nil {
 427  		return nil, err
 428  	}
 429  	return string(jsonBytes), nil
 430  }
 431  
 432  // Response message builders
 433  
 434  func makeOK(eventID string, accepted bool, message string) string {
 435  	msg := []interface{}{"OK", eventID, accepted, message}
 436  	jsonBytes, _ := json.Marshal(msg)
 437  	return string(jsonBytes)
 438  }
 439  
 440  func makeEvent(subID, eventJSON string) string {
 441  	// We return the raw event JSON embedded in the array
 442  	return fmt.Sprintf(`["EVENT","%s",%s]`, subID, eventJSON)
 443  }
 444  
 445  func makeEOSE(subID string) string {
 446  	msg := []interface{}{"EOSE", subID}
 447  	jsonBytes, _ := json.Marshal(msg)
 448  	return string(jsonBytes)
 449  }
 450  
 451  func makeMarkerOK(key string, success bool, message string) string {
 452  	msg := []interface{}{"OK", key, success}
 453  	if message != "" {
 454  		msg = append(msg, message)
 455  	}
 456  	jsonBytes, _ := json.Marshal(msg)
 457  	return string(jsonBytes)
 458  }
 459  
 460  func makeMarkerResult(key string, value *string) string {
 461  	var msg []interface{}
 462  	if value == nil {
 463  		msg = []interface{}{"MARKER", key, nil}
 464  	} else {
 465  		msg = []interface{}{"MARKER", key, *value}
 466  	}
 467  	jsonBytes, _ := json.Marshal(msg)
 468  	return string(jsonBytes)
 469  }
 470  
 471  func makeMarkerHas(key string, has bool) string {
 472  	msg := []interface{}{"MARKER", key, has}
 473  	jsonBytes, _ := json.Marshal(msg)
 474  	return string(jsonBytes)
 475  }
 476  
 477  // Helper functions
 478  
 479  // promiseWrapper wraps a function in a JavaScript Promise
 480  func promiseWrapper(fn func() (interface{}, error)) interface{} {
 481  	handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
 482  		resolve := args[0]
 483  		reject := args[1]
 484  
 485  		go func() {
 486  			result, err := fn()
 487  			if err != nil {
 488  				reject.Invoke(err.Error())
 489  			} else {
 490  				resolve.Invoke(result)
 491  			}
 492  		}()
 493  
 494  		return nil
 495  	})
 496  
 497  	promiseConstructor := js.Global().Get("Promise")
 498  	return promiseConstructor.New(handler)
 499  }
 500  
 501  // rejectPromise creates a rejected promise with an error message
 502  func rejectPromise(msg string) interface{} {
 503  	handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
 504  		reject := args[1]
 505  		reject.Invoke(msg)
 506  		return nil
 507  	})
 508  
 509  	promiseConstructor := js.Global().Get("Promise")
 510  	return promiseConstructor.New(handler)
 511  }
 512  
 513  // parseEventFromRawJSON parses a Nostr event from raw JSON
 514  func parseEventFromRawJSON(raw json.RawMessage) (*event.E, error) {
 515  	return parseEventFromJSON(string(raw))
 516  }
 517  
 518  // parseEventFromJSON parses a Nostr event from JSON
 519  func parseEventFromJSON(jsonStr string) (*event.E, error) {
 520  	// Parse into intermediate struct for JSON compatibility
 521  	var raw struct {
 522  		ID        string     `json:"id"`
 523  		Pubkey    string     `json:"pubkey"`
 524  		CreatedAt int64      `json:"created_at"`
 525  		Kind      int        `json:"kind"`
 526  		Tags      [][]string `json:"tags"`
 527  		Content   string     `json:"content"`
 528  		Sig       string     `json:"sig"`
 529  	}
 530  
 531  	if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil {
 532  		return nil, err
 533  	}
 534  
 535  	ev := &event.E{
 536  		Kind:      uint16(raw.Kind),
 537  		CreatedAt: raw.CreatedAt,
 538  		Content:   []byte(raw.Content),
 539  	}
 540  
 541  	// Decode ID
 542  	if id, err := hex.Dec(raw.ID); err == nil && len(id) == 32 {
 543  		ev.ID = id
 544  	}
 545  
 546  	// Decode Pubkey
 547  	if pk, err := hex.Dec(raw.Pubkey); err == nil && len(pk) == 32 {
 548  		ev.Pubkey = pk
 549  	}
 550  
 551  	// Decode Sig
 552  	if sig, err := hex.Dec(raw.Sig); err == nil && len(sig) == 64 {
 553  		ev.Sig = sig
 554  	}
 555  
 556  	// Convert tags
 557  	if len(raw.Tags) > 0 {
 558  		ev.Tags = tag.NewSWithCap(len(raw.Tags))
 559  		for _, t := range raw.Tags {
 560  			tagBytes := make([][]byte, len(t))
 561  			for i, s := range t {
 562  				tagBytes[i] = []byte(s)
 563  			}
 564  			newTag := tag.NewFromBytesSlice(tagBytes...)
 565  			ev.Tags.Append(newTag)
 566  		}
 567  	}
 568  
 569  	return ev, nil
 570  }
 571  
 572  // parseFilterFromRawJSON parses a Nostr filter from raw JSON
 573  func parseFilterFromRawJSON(raw json.RawMessage) (*filter.F, error) {
 574  	return parseFilterFromJSON(string(raw))
 575  }
 576  
 577  // parseFilterFromJSON parses a Nostr filter from JSON
 578  func parseFilterFromJSON(jsonStr string) (*filter.F, error) {
 579  	// Parse into intermediate struct
 580  	var raw struct {
 581  		IDs     []string `json:"ids,omitempty"`
 582  		Authors []string `json:"authors,omitempty"`
 583  		Kinds   []int    `json:"kinds,omitempty"`
 584  		Since   *int64   `json:"since,omitempty"`
 585  		Until   *int64   `json:"until,omitempty"`
 586  		Limit   *uint    `json:"limit,omitempty"`
 587  		Search  *string  `json:"search,omitempty"`
 588  	}
 589  
 590  	if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil {
 591  		return nil, err
 592  	}
 593  
 594  	f := &filter.F{}
 595  
 596  	// Set IDs
 597  	if len(raw.IDs) > 0 {
 598  		f.Ids = tag.NewWithCap(len(raw.IDs))
 599  		for _, idHex := range raw.IDs {
 600  			f.Ids.T = append(f.Ids.T, []byte(idHex))
 601  		}
 602  	}
 603  
 604  	// Set Authors
 605  	if len(raw.Authors) > 0 {
 606  		f.Authors = tag.NewWithCap(len(raw.Authors))
 607  		for _, pkHex := range raw.Authors {
 608  			f.Authors.T = append(f.Authors.T, []byte(pkHex))
 609  		}
 610  	}
 611  
 612  	// Set Kinds
 613  	if len(raw.Kinds) > 0 {
 614  		f.Kinds = kind.NewWithCap(len(raw.Kinds))
 615  		for _, k := range raw.Kinds {
 616  			f.Kinds.K = append(f.Kinds.K, kind.New(uint16(k)))
 617  		}
 618  	}
 619  
 620  	// Set timestamps
 621  	if raw.Since != nil {
 622  		f.Since = timestamp.New(*raw.Since)
 623  	}
 624  	if raw.Until != nil {
 625  		f.Until = timestamp.New(*raw.Until)
 626  	}
 627  
 628  	// Set limit
 629  	if raw.Limit != nil {
 630  		f.Limit = raw.Limit
 631  	}
 632  
 633  	// Set search
 634  	if raw.Search != nil {
 635  		f.Search = []byte(*raw.Search)
 636  	}
 637  
 638  	// Handle tag filters (e.g., #e, #p, #t)
 639  	var rawMap map[string]interface{}
 640  	json.Unmarshal([]byte(jsonStr), &rawMap)
 641  	for key, val := range rawMap {
 642  		if len(key) == 2 && key[0] == '#' {
 643  			if arr, ok := val.([]interface{}); ok {
 644  				tagFilter := tag.NewWithCap(len(arr) + 1)
 645  				// First element is the tag name (e.g., "e", "p")
 646  				tagFilter.T = append(tagFilter.T, []byte{key[1]})
 647  				for _, v := range arr {
 648  					if s, ok := v.(string); ok {
 649  						tagFilter.T = append(tagFilter.T, []byte(s))
 650  					}
 651  				}
 652  				if f.Tags == nil {
 653  					f.Tags = tag.NewSWithCap(4)
 654  				}
 655  				f.Tags.Append(tagFilter)
 656  			}
 657  		}
 658  	}
 659  
 660  	return f, nil
 661  }
 662  
 663  // eventToJSON converts a Nostr event to JSON
 664  func eventToJSON(ev *event.E) ([]byte, error) {
 665  	// Build tags array
 666  	var tags [][]string
 667  	if ev.Tags != nil {
 668  		for _, t := range *ev.Tags {
 669  			if t == nil {
 670  				continue
 671  			}
 672  			tagStrs := make([]string, len(t.T))
 673  			for i, elem := range t.T {
 674  				tagStrs[i] = string(elem)
 675  			}
 676  			tags = append(tags, tagStrs)
 677  		}
 678  	}
 679  
 680  	raw := struct {
 681  		ID        string     `json:"id"`
 682  		Pubkey    string     `json:"pubkey"`
 683  		CreatedAt int64      `json:"created_at"`
 684  		Kind      int        `json:"kind"`
 685  		Tags      [][]string `json:"tags"`
 686  		Content   string     `json:"content"`
 687  		Sig       string     `json:"sig"`
 688  	}{
 689  		ID:        hex.Enc(ev.ID),
 690  		Pubkey:    hex.Enc(ev.Pubkey),
 691  		CreatedAt: ev.CreatedAt,
 692  		Kind:      int(ev.Kind),
 693  		Tags:      tags,
 694  		Content:   string(ev.Content),
 695  		Sig:       hex.Enc(ev.Sig),
 696  	}
 697  
 698  	buf := new(bytes.Buffer)
 699  	enc := json.NewEncoder(buf)
 700  	enc.SetEscapeHTML(false)
 701  	if err := enc.Encode(raw); err != nil {
 702  		return nil, err
 703  	}
 704  
 705  	// Remove trailing newline from encoder
 706  	result := buf.Bytes()
 707  	if len(result) > 0 && result[len(result)-1] == '\n' {
 708  		result = result[:len(result)-1]
 709  	}
 710  
 711  	return result, nil
 712  }
 713