diff --git a/khatru/nip86.go b/khatru/nip86.go index b74cc42..0bf1558 100644 --- a/khatru/nip86.go +++ b/khatru/nip86.go @@ -48,6 +48,7 @@ type RelayManagementAPI struct { DeleteRole func(ctx context.Context, id string) error AssignRole func(ctx context.Context, pubkey nostr.PubKey, roleID string) error UnassignRole func(ctx context.Context, pubkey nostr.PubKey, roleID string) error + SignEvent func(ctx context.Context, kind nostr.Kind, createdAt nostr.Timestamp, tags nostr.Tags, content string) (nostr.Event, error) Generic func(ctx context.Context, request nip86.Request) (nip86.Response, error) } @@ -375,6 +376,14 @@ func (rl *Relay) HandleNIP86(w http.ResponseWriter, r *http.Request) { } else { resp.Result = true } + case nip86.SignEvent: + if rl.ManagementAPI.SignEvent == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if result, err := rl.ManagementAPI.SignEvent(ctx, thing.Kind, thing.CreatedAt, thing.Tags, thing.Content); err != nil { + resp.Error = err.Error() + } else { + resp.Result = result + } case nip86.ListDisallowedKinds: if rl.ManagementAPI.ListDisallowedKinds == nil { resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) diff --git a/nip86/methods.go b/nip86/methods.go index 63e2f92..f6c8024 100644 --- a/nip86/methods.go +++ b/nip86/methods.go @@ -330,6 +330,32 @@ func DecodeRequest(req Request) (MethodParams, error) { return nil, fmt.Errorf("missing role id param for '%s'", req.Method) } return UnassignRole{pk, roleID}, nil + case "signevent": + if len(req.Params) == 0 { + return nil, fmt.Errorf("invalid number of params for '%s'", req.Method) + } + tmpl, ok := req.Params[0].(map[string]any) + if !ok { + return nil, fmt.Errorf("missing event param for '%s'", req.Method) + } + kind, ok := tmpl["kind"].(float64) + if !ok || math.Trunc(kind) != kind { + return nil, fmt.Errorf("invalid kind '%v' for '%s'", tmpl["kind"], req.Method) + } + var createdAt nostr.Timestamp + if ca, ok := tmpl["created_at"]; ok { + caf, ok := ca.(float64) + if !ok || math.Trunc(caf) != caf { + return nil, fmt.Errorf("invalid created_at '%v' for '%s'", ca, req.Method) + } + createdAt = nostr.Timestamp(caf) + } + tags, err := coerceTags(tmpl["tags"]) + if err != nil { + return nil, fmt.Errorf("invalid tags for '%s': %w", req.Method, err) + } + content, _ := tmpl["content"].(string) + return SignEvent{nostr.Kind(kind), createdAt, tags, content}, nil case "stats": return Stats{}, nil default: @@ -350,6 +376,35 @@ func coerceInt(v any) int { return 0 } +// coerceTags converts a decoded JSON value (an array of arrays of strings) into +// nostr.Tags. A nil value yields nil tags; any other shape is an error. +func coerceTags(v any) (nostr.Tags, error) { + if v == nil { + return nil, nil + } + rawTags, ok := v.([]any) + if !ok { + return nil, fmt.Errorf("tags must be an array") + } + tags := make(nostr.Tags, len(rawTags)) + for i, rt := range rawTags { + rawTag, ok := rt.([]any) + if !ok { + return nil, fmt.Errorf("tag %d must be an array", i) + } + tag := make(nostr.Tag, len(rawTag)) + for j, el := range rawTag { + s, ok := el.(string) + if !ok { + return nil, fmt.Errorf("tag %d element %d must be a string", i, j) + } + tag[j] = s + } + tags[i] = tag + } + return tags, nil +} + type MethodParams interface { MethodName() string } @@ -384,6 +439,7 @@ var ( _ MethodParams = (*DeleteRole)(nil) _ MethodParams = (*AssignRole)(nil) _ MethodParams = (*UnassignRole)(nil) + _ MethodParams = (*SignEvent)(nil) _ MethodParams = (*Stats)(nil) ) @@ -563,6 +619,15 @@ type UnassignRole struct { func (UnassignRole) MethodName() string { return "unassignrole" } +type SignEvent struct { + Kind nostr.Kind + CreatedAt nostr.Timestamp + Tags nostr.Tags + Content string +} + +func (SignEvent) MethodName() string { return "signevent" } + type Stats struct{} func (Stats) MethodName() string { return "stats" }