You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
712 lines
18 KiB
712 lines
18 KiB
//go:build js && wasm |
|
|
|
package wasmdb |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"encoding/json" |
|
"fmt" |
|
"syscall/js" |
|
|
|
"git.mleku.dev/mleku/nostr/encoders/event" |
|
"git.mleku.dev/mleku/nostr/encoders/filter" |
|
"git.mleku.dev/mleku/nostr/encoders/hex" |
|
"git.mleku.dev/mleku/nostr/encoders/kind" |
|
"git.mleku.dev/mleku/nostr/encoders/tag" |
|
"git.mleku.dev/mleku/nostr/encoders/timestamp" |
|
) |
|
|
|
// JSBridge holds the database instance for JavaScript access |
|
var jsBridge *JSBridge |
|
|
|
// JSBridge wraps the WasmDB instance for JavaScript interop |
|
// Exposes a relay protocol interface (NIP-01) rather than direct database access |
|
type JSBridge struct { |
|
db *W |
|
ctx context.Context |
|
cancel context.CancelFunc |
|
} |
|
|
|
// RegisterJSBridge exposes the relay protocol API to JavaScript |
|
func RegisterJSBridge(db *W, ctx context.Context, cancel context.CancelFunc) { |
|
jsBridge = &JSBridge{ |
|
db: db, |
|
ctx: ctx, |
|
cancel: cancel, |
|
} |
|
|
|
// Create the wasmdb global object with relay protocol interface |
|
wasmdbObj := map[string]interface{}{ |
|
// Lifecycle |
|
"isReady": js.FuncOf(jsBridge.jsIsReady), |
|
"close": js.FuncOf(jsBridge.jsClose), |
|
"wipe": js.FuncOf(jsBridge.jsWipe), |
|
|
|
// Relay Protocol (NIP-01) |
|
// This is the main entry point - handles EVENT, REQ, CLOSE messages |
|
"handleMessage": js.FuncOf(jsBridge.jsHandleMessage), |
|
|
|
// Graph Query Extensions |
|
"queryGraph": js.FuncOf(jsBridge.jsQueryGraph), |
|
|
|
// Marker Extensions (key-value storage via relay protocol) |
|
// ["MARKER", "set", key, value] -> ["OK", key, true] |
|
// ["MARKER", "get", key] -> ["MARKER", key, value] |
|
// ["MARKER", "delete", key] -> ["OK", key, true] |
|
// These are also handled via handleMessage |
|
} |
|
|
|
js.Global().Set("wasmdb", wasmdbObj) |
|
} |
|
|
|
// jsIsReady returns true if the database is ready |
|
func (b *JSBridge) jsIsReady(this js.Value, args []js.Value) interface{} { |
|
select { |
|
case <-b.db.Ready(): |
|
return true |
|
default: |
|
return false |
|
} |
|
} |
|
|
|
// jsClose closes the database |
|
func (b *JSBridge) jsClose(this js.Value, args []js.Value) interface{} { |
|
return promiseWrapper(func() (interface{}, error) { |
|
err := b.db.Close() |
|
return nil, err |
|
}) |
|
} |
|
|
|
// jsWipe wipes all data from the database |
|
func (b *JSBridge) jsWipe(this js.Value, args []js.Value) interface{} { |
|
return promiseWrapper(func() (interface{}, error) { |
|
err := b.db.Wipe() |
|
return nil, err |
|
}) |
|
} |
|
|
|
// jsHandleMessage handles NIP-01 relay protocol messages |
|
// Input: JSON string representing a relay message array |
|
// |
|
// ["EVENT", <event>] - Submit an event |
|
// ["REQ", <sub_id>, <filter>...] - Request events |
|
// ["CLOSE", <sub_id>] - Close a subscription |
|
// ["MARKER", "set"|"get"|"delete", key, value?] - Marker operations |
|
// |
|
// Output: Promise<string[]> - Array of JSON response messages |
|
func (b *JSBridge) jsHandleMessage(this js.Value, args []js.Value) interface{} { |
|
if len(args) < 1 { |
|
return rejectPromise("handleMessage requires message JSON argument") |
|
} |
|
|
|
messageJSON := args[0].String() |
|
|
|
return promiseWrapper(func() (interface{}, error) { |
|
// Parse the message array |
|
var message []json.RawMessage |
|
if err := json.Unmarshal([]byte(messageJSON), &message); err != nil { |
|
return nil, fmt.Errorf("invalid message format: %w", err) |
|
} |
|
|
|
if len(message) < 1 { |
|
return nil, fmt.Errorf("empty message") |
|
} |
|
|
|
// Get message type |
|
var msgType string |
|
if err := json.Unmarshal(message[0], &msgType); err != nil { |
|
return nil, fmt.Errorf("invalid message type: %w", err) |
|
} |
|
|
|
switch msgType { |
|
case "EVENT": |
|
return b.handleEvent(message) |
|
case "REQ": |
|
return b.handleReq(message) |
|
case "CLOSE": |
|
return b.handleClose(message) |
|
case "MARKER": |
|
return b.handleMarker(message) |
|
default: |
|
return nil, fmt.Errorf("unknown message type: %s", msgType) |
|
} |
|
}) |
|
} |
|
|
|
// handleEvent processes an EVENT message |
|
// ["EVENT", <event>] -> ["OK", <id>, true/false, "message"] |
|
func (b *JSBridge) handleEvent(message []json.RawMessage) (interface{}, error) { |
|
if len(message) < 2 { |
|
return []interface{}{makeOK("", false, "missing event")}, nil |
|
} |
|
|
|
// Parse the event |
|
ev, err := parseEventFromRawJSON(message[1]) |
|
if err != nil { |
|
return []interface{}{makeOK("", false, fmt.Sprintf("invalid event: %s", err))}, nil |
|
} |
|
|
|
eventIDHex := hex.Enc(ev.ID) |
|
|
|
// Save to database |
|
replaced, err := b.db.SaveEvent(b.ctx, ev) |
|
if err != nil { |
|
return []interface{}{makeOK(eventIDHex, false, err.Error())}, nil |
|
} |
|
|
|
var msg string |
|
if replaced { |
|
msg = "replaced" |
|
} else { |
|
msg = "saved" |
|
} |
|
|
|
return []interface{}{makeOK(eventIDHex, true, msg)}, nil |
|
} |
|
|
|
// handleReq processes a REQ message |
|
// ["REQ", <sub_id>, <filter>...] -> ["EVENT", <sub_id>, <event>]..., ["EOSE", <sub_id>] |
|
func (b *JSBridge) handleReq(message []json.RawMessage) (interface{}, error) { |
|
if len(message) < 2 { |
|
return nil, fmt.Errorf("REQ requires subscription ID") |
|
} |
|
|
|
// Get subscription ID |
|
var subID string |
|
if err := json.Unmarshal(message[1], &subID); err != nil { |
|
return nil, fmt.Errorf("invalid subscription ID: %w", err) |
|
} |
|
|
|
// Parse filters (can have multiple) |
|
var allEvents []*event.E |
|
for i := 2; i < len(message); i++ { |
|
f, err := parseFilterFromRawJSON(message[i]) |
|
if err != nil { |
|
continue |
|
} |
|
|
|
events, err := b.db.QueryEvents(b.ctx, f) |
|
if err != nil { |
|
continue |
|
} |
|
|
|
allEvents = append(allEvents, events...) |
|
} |
|
|
|
// Build response messages |
|
responses := make([]interface{}, 0, len(allEvents)+1) |
|
|
|
// Add EVENT messages |
|
for _, ev := range allEvents { |
|
eventJSON, err := eventToJSON(ev) |
|
if err != nil { |
|
continue |
|
} |
|
responses = append(responses, makeEvent(subID, string(eventJSON))) |
|
} |
|
|
|
// Add EOSE |
|
responses = append(responses, makeEOSE(subID)) |
|
|
|
return responses, nil |
|
} |
|
|
|
// handleClose processes a CLOSE message |
|
// ["CLOSE", <sub_id>] -> (no response for local relay) |
|
func (b *JSBridge) handleClose(message []json.RawMessage) (interface{}, error) { |
|
// For the local relay, subscriptions are stateless (single query/response) |
|
// CLOSE is a no-op but we acknowledge it |
|
return []interface{}{}, nil |
|
} |
|
|
|
// handleMarker processes MARKER extension messages |
|
// ["MARKER", "set", key, value] -> ["OK", key, true] |
|
// ["MARKER", "get", key] -> ["MARKER", key, value] or ["MARKER", key, null] |
|
// ["MARKER", "delete", key] -> ["OK", key, true] |
|
func (b *JSBridge) handleMarker(message []json.RawMessage) (interface{}, error) { |
|
if len(message) < 3 { |
|
return nil, fmt.Errorf("MARKER requires operation and key") |
|
} |
|
|
|
var operation string |
|
if err := json.Unmarshal(message[1], &operation); err != nil { |
|
return nil, fmt.Errorf("invalid marker operation: %w", err) |
|
} |
|
|
|
var key string |
|
if err := json.Unmarshal(message[2], &key); err != nil { |
|
return nil, fmt.Errorf("invalid marker key: %w", err) |
|
} |
|
|
|
switch operation { |
|
case "set": |
|
if len(message) < 4 { |
|
return nil, fmt.Errorf("MARKER set requires value") |
|
} |
|
var value string |
|
if err := json.Unmarshal(message[3], &value); err != nil { |
|
return nil, fmt.Errorf("invalid marker value: %w", err) |
|
} |
|
if err := b.db.SetMarker(key, []byte(value)); err != nil { |
|
return []interface{}{makeMarkerOK(key, false, err.Error())}, nil |
|
} |
|
return []interface{}{makeMarkerOK(key, true, "")}, nil |
|
|
|
case "get": |
|
value, err := b.db.GetMarker(key) |
|
if err != nil || value == nil { |
|
return []interface{}{makeMarkerResult(key, nil)}, nil |
|
} |
|
valueStr := string(value) |
|
return []interface{}{makeMarkerResult(key, &valueStr)}, nil |
|
|
|
case "delete": |
|
if err := b.db.DeleteMarker(key); err != nil { |
|
return []interface{}{makeMarkerOK(key, false, err.Error())}, nil |
|
} |
|
return []interface{}{makeMarkerOK(key, true, "")}, nil |
|
|
|
case "has": |
|
has := b.db.HasMarker(key) |
|
return []interface{}{makeMarkerHas(key, has)}, nil |
|
|
|
default: |
|
return nil, fmt.Errorf("unknown marker operation: %s", operation) |
|
} |
|
} |
|
|
|
// jsQueryGraph handles graph query extensions |
|
// Args: [queryJSON: string] - JSON-encoded graph query |
|
// Returns: Promise<string> - JSON-encoded graph result |
|
func (b *JSBridge) jsQueryGraph(this js.Value, args []js.Value) interface{} { |
|
if len(args) < 1 { |
|
return rejectPromise("queryGraph requires query JSON argument") |
|
} |
|
|
|
queryJSON := args[0].String() |
|
|
|
return promiseWrapper(func() (interface{}, error) { |
|
var query struct { |
|
Type string `json:"type"` |
|
Pubkey string `json:"pubkey"` |
|
Depth int `json:"depth,omitempty"` |
|
Limit int `json:"limit,omitempty"` |
|
} |
|
|
|
if err := json.Unmarshal([]byte(queryJSON), &query); err != nil { |
|
return nil, fmt.Errorf("invalid graph query: %w", err) |
|
} |
|
|
|
// Set defaults |
|
if query.Depth == 0 { |
|
query.Depth = 1 |
|
} |
|
if query.Limit == 0 { |
|
query.Limit = 1000 |
|
} |
|
|
|
switch query.Type { |
|
case "follows": |
|
return b.queryFollows(query.Pubkey, query.Depth, query.Limit) |
|
case "followers": |
|
return b.queryFollowers(query.Pubkey, query.Limit) |
|
case "mutes": |
|
return b.queryMutes(query.Pubkey) |
|
default: |
|
return nil, fmt.Errorf("unknown graph query type: %s", query.Type) |
|
} |
|
}) |
|
} |
|
|
|
// queryFollows returns who a pubkey follows |
|
func (b *JSBridge) queryFollows(pubkeyHex string, depth, limit int) (interface{}, error) { |
|
// Query kind 3 (contact list) for the pubkey |
|
f := &filter.F{ |
|
Kinds: kind.NewWithCap(1), |
|
} |
|
f.Kinds.K = append(f.Kinds.K, kind.New(3)) |
|
f.Authors = tag.NewWithCap(1) |
|
f.Authors.T = append(f.Authors.T, []byte(pubkeyHex)) |
|
one := uint(1) |
|
f.Limit = &one |
|
|
|
events, err := b.db.QueryEvents(b.ctx, f) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
var follows []string |
|
if len(events) > 0 && events[0].Tags != nil { |
|
for _, t := range *events[0].Tags { |
|
if t != nil && len(t.T) >= 2 && string(t.T[0]) == "p" { |
|
follows = append(follows, string(t.T[1])) |
|
} |
|
} |
|
} |
|
|
|
result := map[string]interface{}{ |
|
"nodes": follows, |
|
} |
|
jsonBytes, err := json.Marshal(result) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return string(jsonBytes), nil |
|
} |
|
|
|
// queryFollowers returns who follows a pubkey |
|
func (b *JSBridge) queryFollowers(pubkeyHex string, limit int) (interface{}, error) { |
|
// Query kind 3 events that tag this pubkey |
|
f := &filter.F{ |
|
Kinds: kind.NewWithCap(1), |
|
Tags: tag.NewSWithCap(1), |
|
} |
|
f.Kinds.K = append(f.Kinds.K, kind.New(3)) |
|
|
|
// Add #p tag filter |
|
pTag := tag.NewWithCap(2) |
|
pTag.T = append(pTag.T, []byte("p")) |
|
pTag.T = append(pTag.T, []byte(pubkeyHex)) |
|
f.Tags.Append(pTag) |
|
|
|
lim := uint(limit) |
|
f.Limit = &lim |
|
|
|
events, err := b.db.QueryEvents(b.ctx, f) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
var followers []string |
|
for _, ev := range events { |
|
followers = append(followers, hex.Enc(ev.Pubkey)) |
|
} |
|
|
|
result := map[string]interface{}{ |
|
"nodes": followers, |
|
} |
|
jsonBytes, err := json.Marshal(result) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return string(jsonBytes), nil |
|
} |
|
|
|
// queryMutes returns who a pubkey has muted |
|
func (b *JSBridge) queryMutes(pubkeyHex string) (interface{}, error) { |
|
// Query kind 10000 (mute list) for the pubkey |
|
f := &filter.F{ |
|
Kinds: kind.NewWithCap(1), |
|
} |
|
f.Kinds.K = append(f.Kinds.K, kind.New(10000)) |
|
f.Authors = tag.NewWithCap(1) |
|
f.Authors.T = append(f.Authors.T, []byte(pubkeyHex)) |
|
one := uint(1) |
|
f.Limit = &one |
|
|
|
events, err := b.db.QueryEvents(b.ctx, f) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
var mutes []string |
|
if len(events) > 0 && events[0].Tags != nil { |
|
for _, t := range *events[0].Tags { |
|
if t != nil && len(t.T) >= 2 && string(t.T[0]) == "p" { |
|
mutes = append(mutes, string(t.T[1])) |
|
} |
|
} |
|
} |
|
|
|
result := map[string]interface{}{ |
|
"nodes": mutes, |
|
} |
|
jsonBytes, err := json.Marshal(result) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return string(jsonBytes), nil |
|
} |
|
|
|
// Response message builders |
|
|
|
func makeOK(eventID string, accepted bool, message string) string { |
|
msg := []interface{}{"OK", eventID, accepted, message} |
|
jsonBytes, _ := json.Marshal(msg) |
|
return string(jsonBytes) |
|
} |
|
|
|
func makeEvent(subID, eventJSON string) string { |
|
// We return the raw event JSON embedded in the array |
|
return fmt.Sprintf(`["EVENT","%s",%s]`, subID, eventJSON) |
|
} |
|
|
|
func makeEOSE(subID string) string { |
|
msg := []interface{}{"EOSE", subID} |
|
jsonBytes, _ := json.Marshal(msg) |
|
return string(jsonBytes) |
|
} |
|
|
|
func makeMarkerOK(key string, success bool, message string) string { |
|
msg := []interface{}{"OK", key, success} |
|
if message != "" { |
|
msg = append(msg, message) |
|
} |
|
jsonBytes, _ := json.Marshal(msg) |
|
return string(jsonBytes) |
|
} |
|
|
|
func makeMarkerResult(key string, value *string) string { |
|
var msg []interface{} |
|
if value == nil { |
|
msg = []interface{}{"MARKER", key, nil} |
|
} else { |
|
msg = []interface{}{"MARKER", key, *value} |
|
} |
|
jsonBytes, _ := json.Marshal(msg) |
|
return string(jsonBytes) |
|
} |
|
|
|
func makeMarkerHas(key string, has bool) string { |
|
msg := []interface{}{"MARKER", key, has} |
|
jsonBytes, _ := json.Marshal(msg) |
|
return string(jsonBytes) |
|
} |
|
|
|
// Helper functions |
|
|
|
// promiseWrapper wraps a function in a JavaScript Promise |
|
func promiseWrapper(fn func() (interface{}, error)) interface{} { |
|
handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { |
|
resolve := args[0] |
|
reject := args[1] |
|
|
|
go func() { |
|
result, err := fn() |
|
if err != nil { |
|
reject.Invoke(err.Error()) |
|
} else { |
|
resolve.Invoke(result) |
|
} |
|
}() |
|
|
|
return nil |
|
}) |
|
|
|
promiseConstructor := js.Global().Get("Promise") |
|
return promiseConstructor.New(handler) |
|
} |
|
|
|
// rejectPromise creates a rejected promise with an error message |
|
func rejectPromise(msg string) interface{} { |
|
handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { |
|
reject := args[1] |
|
reject.Invoke(msg) |
|
return nil |
|
}) |
|
|
|
promiseConstructor := js.Global().Get("Promise") |
|
return promiseConstructor.New(handler) |
|
} |
|
|
|
// parseEventFromRawJSON parses a Nostr event from raw JSON |
|
func parseEventFromRawJSON(raw json.RawMessage) (*event.E, error) { |
|
return parseEventFromJSON(string(raw)) |
|
} |
|
|
|
// parseEventFromJSON parses a Nostr event from JSON |
|
func parseEventFromJSON(jsonStr string) (*event.E, error) { |
|
// Parse into intermediate struct for JSON compatibility |
|
var raw struct { |
|
ID string `json:"id"` |
|
Pubkey string `json:"pubkey"` |
|
CreatedAt int64 `json:"created_at"` |
|
Kind int `json:"kind"` |
|
Tags [][]string `json:"tags"` |
|
Content string `json:"content"` |
|
Sig string `json:"sig"` |
|
} |
|
|
|
if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil { |
|
return nil, err |
|
} |
|
|
|
ev := &event.E{ |
|
Kind: uint16(raw.Kind), |
|
CreatedAt: raw.CreatedAt, |
|
Content: []byte(raw.Content), |
|
} |
|
|
|
// Decode ID |
|
if id, err := hex.Dec(raw.ID); err == nil && len(id) == 32 { |
|
ev.ID = id |
|
} |
|
|
|
// Decode Pubkey |
|
if pk, err := hex.Dec(raw.Pubkey); err == nil && len(pk) == 32 { |
|
ev.Pubkey = pk |
|
} |
|
|
|
// Decode Sig |
|
if sig, err := hex.Dec(raw.Sig); err == nil && len(sig) == 64 { |
|
ev.Sig = sig |
|
} |
|
|
|
// Convert tags |
|
if len(raw.Tags) > 0 { |
|
ev.Tags = tag.NewSWithCap(len(raw.Tags)) |
|
for _, t := range raw.Tags { |
|
tagBytes := make([][]byte, len(t)) |
|
for i, s := range t { |
|
tagBytes[i] = []byte(s) |
|
} |
|
newTag := tag.NewFromBytesSlice(tagBytes...) |
|
ev.Tags.Append(newTag) |
|
} |
|
} |
|
|
|
return ev, nil |
|
} |
|
|
|
// parseFilterFromRawJSON parses a Nostr filter from raw JSON |
|
func parseFilterFromRawJSON(raw json.RawMessage) (*filter.F, error) { |
|
return parseFilterFromJSON(string(raw)) |
|
} |
|
|
|
// parseFilterFromJSON parses a Nostr filter from JSON |
|
func parseFilterFromJSON(jsonStr string) (*filter.F, error) { |
|
// Parse into intermediate struct |
|
var raw struct { |
|
IDs []string `json:"ids,omitempty"` |
|
Authors []string `json:"authors,omitempty"` |
|
Kinds []int `json:"kinds,omitempty"` |
|
Since *int64 `json:"since,omitempty"` |
|
Until *int64 `json:"until,omitempty"` |
|
Limit *uint `json:"limit,omitempty"` |
|
Search *string `json:"search,omitempty"` |
|
} |
|
|
|
if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil { |
|
return nil, err |
|
} |
|
|
|
f := &filter.F{} |
|
|
|
// Set IDs |
|
if len(raw.IDs) > 0 { |
|
f.Ids = tag.NewWithCap(len(raw.IDs)) |
|
for _, idHex := range raw.IDs { |
|
f.Ids.T = append(f.Ids.T, []byte(idHex)) |
|
} |
|
} |
|
|
|
// Set Authors |
|
if len(raw.Authors) > 0 { |
|
f.Authors = tag.NewWithCap(len(raw.Authors)) |
|
for _, pkHex := range raw.Authors { |
|
f.Authors.T = append(f.Authors.T, []byte(pkHex)) |
|
} |
|
} |
|
|
|
// Set Kinds |
|
if len(raw.Kinds) > 0 { |
|
f.Kinds = kind.NewWithCap(len(raw.Kinds)) |
|
for _, k := range raw.Kinds { |
|
f.Kinds.K = append(f.Kinds.K, kind.New(uint16(k))) |
|
} |
|
} |
|
|
|
// Set timestamps |
|
if raw.Since != nil { |
|
f.Since = timestamp.New(*raw.Since) |
|
} |
|
if raw.Until != nil { |
|
f.Until = timestamp.New(*raw.Until) |
|
} |
|
|
|
// Set limit |
|
if raw.Limit != nil { |
|
f.Limit = raw.Limit |
|
} |
|
|
|
// Set search |
|
if raw.Search != nil { |
|
f.Search = []byte(*raw.Search) |
|
} |
|
|
|
// Handle tag filters (e.g., #e, #p, #t) |
|
var rawMap map[string]interface{} |
|
json.Unmarshal([]byte(jsonStr), &rawMap) |
|
for key, val := range rawMap { |
|
if len(key) == 2 && key[0] == '#' { |
|
if arr, ok := val.([]interface{}); ok { |
|
tagFilter := tag.NewWithCap(len(arr) + 1) |
|
// First element is the tag name (e.g., "e", "p") |
|
tagFilter.T = append(tagFilter.T, []byte{key[1]}) |
|
for _, v := range arr { |
|
if s, ok := v.(string); ok { |
|
tagFilter.T = append(tagFilter.T, []byte(s)) |
|
} |
|
} |
|
if f.Tags == nil { |
|
f.Tags = tag.NewSWithCap(4) |
|
} |
|
f.Tags.Append(tagFilter) |
|
} |
|
} |
|
} |
|
|
|
return f, nil |
|
} |
|
|
|
// eventToJSON converts a Nostr event to JSON |
|
func eventToJSON(ev *event.E) ([]byte, error) { |
|
// Build tags array |
|
var tags [][]string |
|
if ev.Tags != nil { |
|
for _, t := range *ev.Tags { |
|
if t == nil { |
|
continue |
|
} |
|
tagStrs := make([]string, len(t.T)) |
|
for i, elem := range t.T { |
|
tagStrs[i] = string(elem) |
|
} |
|
tags = append(tags, tagStrs) |
|
} |
|
} |
|
|
|
raw := struct { |
|
ID string `json:"id"` |
|
Pubkey string `json:"pubkey"` |
|
CreatedAt int64 `json:"created_at"` |
|
Kind int `json:"kind"` |
|
Tags [][]string `json:"tags"` |
|
Content string `json:"content"` |
|
Sig string `json:"sig"` |
|
}{ |
|
ID: hex.Enc(ev.ID), |
|
Pubkey: hex.Enc(ev.Pubkey), |
|
CreatedAt: ev.CreatedAt, |
|
Kind: int(ev.Kind), |
|
Tags: tags, |
|
Content: string(ev.Content), |
|
Sig: hex.Enc(ev.Sig), |
|
} |
|
|
|
buf := new(bytes.Buffer) |
|
enc := json.NewEncoder(buf) |
|
enc.SetEscapeHTML(false) |
|
if err := enc.Encode(raw); err != nil { |
|
return nil, err |
|
} |
|
|
|
// Remove trailing newline from encoder |
|
result := buf.Bytes() |
|
if len(result) > 0 && result[len(result)-1] == '\n' { |
|
result = result[:len(result)-1] |
|
} |
|
|
|
return result, nil |
|
}
|
|
|