Files
realy/openapi/http-event.go
2025-06-24 18:39:32 +01:00

195 lines
5.8 KiB
Go

package openapi
import (
"bytes"
"fmt"
"net/http"
"github.com/danielgtaylor/huma/v2"
"realy.lol/chk"
"realy.lol/context"
"realy.lol/event"
"realy.lol/filter"
"realy.lol/hex"
"realy.lol/ints"
"realy.lol/kind"
"realy.lol/log"
"realy.lol/realy/helpers"
"realy.lol/sha256"
"realy.lol/tag"
)
// EventInput is the parameters for the Event HTTP API method.
type EventInput struct {
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"false"`
Accept string `header:"Accept" default:"application/nostr+json;q=0.9,application/x-realy-event:q=0.1"`
RawBody []byte
}
// EventOutput is the return parameters for the HTTP API Event method.
type EventOutput struct{ Body string }
// RegisterEvent is the implementatino of the HTTP API Event method.
func (x *Operations) RegisterEvent(api huma.API) {
name := "Event"
description := "Submit an event"
path := x.path + "/event"
scopes := []string{"user", "write"}
method := http.MethodPost
huma.Register(api, huma.Operation{
OperationID: name,
Summary: name,
Path: path,
Method: method,
Tags: []string{"events"},
Description: helpers.GenerateDescription(description, scopes),
Security: []map[string][]string{{"auth": scopes}},
}, func(ctx context.T, input *EventInput) (output *EventOutput, err error) {
r := ctx.Value("http-request").(*http.Request)
// w := ctx.Value("http-response").(http.ResponseWriter)
remote := helpers.GetRemoteFromReq(r)
ev := &event.T{}
if _, err = ev.Unmarshal(input.RawBody); chk.E(err) {
err = huma.Error406NotAcceptable(err.Error())
return
}
var ok bool
sto := x.Storage()
if sto == nil {
panic("no event store has been set to store event")
}
accept, notice, after := x.AcceptEvent(ctx, ev, r, remote)
if !accept {
err = huma.Error401Unauthorized(notice)
return
}
if !bytes.Equal(ev.GetIDBytes(), ev.Id) {
err = huma.Error400BadRequest("event id is computed incorrectly")
return
}
if ok, err = ev.Verify(); chk.T(err) {
err = huma.Error400BadRequest("failed to verify signature")
return
} else if !ok {
err = huma.Error400BadRequest("signature is invalid")
return
}
if ev.Kind.K == kind.Deletion.K {
log.I.F("delete event\n%s", ev.Serialize())
for _, t := range ev.Tags.ToSliceOfTags() {
var res []*event.T
if t.Len() >= 2 {
switch {
case bytes.Equal(t.Key(), []byte("e")):
evId := make([]byte, sha256.Size)
if _, err = hex.DecBytes(evId, t.Value()); chk.E(err) {
continue
}
res, err = sto.QueryEvents(ctx, &filter.T{IDs: tag.New(evId)})
if err != nil {
err = huma.Error500InternalServerError(err.Error())
return
}
for i := range res {
if res[i].Kind.Equal(kind.Deletion) {
err = huma.Error409Conflict("not processing or storing delete event containing delete event references")
}
if !bytes.Equal(res[i].Pubkey, ev.Pubkey) {
err = huma.Error409Conflict("cannot delete other users' events (delete by e tag)")
return
}
}
case bytes.Equal(t.Key(), []byte("a")):
split := bytes.Split(t.Value(), []byte{':'})
if len(split) != 3 {
continue
}
var pk []byte
if pk, err = hex.DecAppend(nil, split[1]); chk.E(err) {
err = huma.Error400BadRequest(fmt.Sprintf("delete event a tag pubkey value invalid: %s",
t.Value()))
return
}
kin := ints.New(uint16(0))
if _, err = kin.Unmarshal(split[0]); chk.E(err) {
err = huma.Error400BadRequest(fmt.Sprintf("delete event a tag kind value invalid: %s",
t.Value()))
return
}
kk := kind.New(kin.Uint16())
if kk.Equal(kind.Deletion) {
err = huma.Error403Forbidden("delete event kind may not be deleted")
return
}
if !kk.IsParameterizedReplaceable() {
err = huma.Error403Forbidden("delete tags with a tags containing non-parameterized-replaceable events cannot be processed")
return
}
if !bytes.Equal(pk, ev.Pubkey) {
log.I.S(pk, ev.Pubkey, ev)
err = huma.Error403Forbidden("cannot delete other users' events (delete by a tag)")
return
}
f := filter.New()
f.Kinds.K = []*kind.T{kk}
f.Authors.Append(pk)
f.Tags.AppendTags(tag.New([]byte{'#', 'd'}, split[2]))
res, err = sto.QueryEvents(ctx, f)
if err != nil {
err = huma.Error500InternalServerError(err.Error())
return
}
}
}
if len(res) < 1 {
continue
}
var resTmp []*event.T
for _, v := range res {
if ev.CreatedAt.U64() >= v.CreatedAt.U64() {
resTmp = append(resTmp, v)
}
}
res = resTmp
for _, target := range res {
if target.Kind.K == kind.Deletion.K {
err = huma.Error403Forbidden(fmt.Sprintf(
"cannot delete delete event %s", ev.Id))
return
}
if target.CreatedAt.Int() > ev.CreatedAt.Int() {
// todo: shouldn't this be an error?
log.I.F("not deleting\n%d%\nbecause delete event is older\n%d",
target.CreatedAt.Int(), ev.CreatedAt.Int())
continue
}
if !bytes.Equal(target.Pubkey, ev.Pubkey) {
err = huma.Error403Forbidden("only author can delete event")
return
}
if err = sto.DeleteEvent(ctx, target.EventId()); chk.T(err) {
err = huma.Error500InternalServerError(err.Error())
return
}
}
res = nil
}
return
}
var reason []byte
ok, reason = x.AddEvent(ctx, ev, r, remote)
// return the response whether true or false and any reason if false
if ok {
} else {
err = huma.Error500InternalServerError(string(reason))
}
if after != nil {
// do this in the background and let the http response close
go after()
}
output = &EventOutput{"event accepted"}
return
})
}