diff --git a/pkg/encoders/envelopes/messages/messages.go b/pkg/encoders/envelopes/messages/messages.go new file mode 100644 index 0000000..4d87374 --- /dev/null +++ b/pkg/encoders/envelopes/messages/messages.go @@ -0,0 +1,54 @@ +// Package messages is a collection of example/common messages and +// machine-readable prefixes to use with OK and CLOSED envelopes. +package messages + +import ( + "lukechampine.com/frand" +) + +const ( + // Duplicate is a machine readable prefix for OK envelopes indicating that the + // submitted event is already in the relay,s event store. + Duplicate = "duplicate" + + // Pow is a machine readable prefix for OK envelopes indicating that the + // eventid.T lacks sufficient zeros at the front. + Pow = "pow" + + // Blocked is a machine readable prefix for OK envelopes indicating the event + // submission or REQ has been rejected. + Blocked = "blocked" + + // RateLimited is a machine readable prefix for CLOSED and OK envelopes + // indicating the relay is now slowing down processing of requests from the + // client. + RateLimited = "rate-limited" + + // Invalid is a machine readable prefix for OK envelopes indicating + // that the submitted event or other request is not correctly formatted, and may + // mean a signature does not verify. + Invalid = "invalid" + + // Error is a machine readable prefix for CLOSED and OK envelopes indicating + // there was some kind of error in processing the request. + Error = "error" +) + +// Examples are some examples of the use of the prefixes above with appropriate +// human-readable suffixes. +var Examples = [][]byte{ + []byte("pow: difficulty 25>=24"), + []byte("duplicate: already have this event"), + []byte("blocked: you are banned from posting here"), + []byte("blocked: please register your pubkey at " + + "https://my-expensive-relay.example.com"), + []byte("rate-limited: slow down there chief"), + []byte("invalid: event creation date is too far off from the current time"), + []byte("pow: difficulty 26 is less than 30"), + []byte("error: could not connect to the database"), +} + +// RandomMessage generates a random message out of the above list of Examples. +func RandomMessage() []byte { + return Examples[frand.Intn(len(Examples)-1)] +} diff --git a/pkg/encoders/envelopes/noticeenvelope/noticeenvelope.go b/pkg/encoders/envelopes/noticeenvelope/noticeenvelope.go new file mode 100644 index 0000000..7e25f26 --- /dev/null +++ b/pkg/encoders/envelopes/noticeenvelope/noticeenvelope.go @@ -0,0 +1,84 @@ +// Package noticeenvelope is a codec for the NOTICE envelope, which is used to +// serve (mostly ignored) messages that are supposed to be shown to a user in +// the client. +package noticeenvelope + +import ( + "io" + + "lol.mleku.dev/chk" + "next.orly.dev/pkg/encoders/envelopes" + "next.orly.dev/pkg/encoders/text" + "next.orly.dev/pkg/interfaces/codec" +) + +// L is the label associated with this type of codec.Envelope. +const L = "NOTICE" + +// T is a NOTICE envelope, intended to convey information to the user about the +// state of the relay connection. This thing is rarely displayed on clients +// except sometimes in event logs. +type T struct { + Message []byte +} + +var _ codec.Envelope = (*T)(nil) + +// New creates a new empty NOTICE noticeenvelope.T. +func New() *T { return &T{} } + +// NewFrom creates a new noticeenvelope.T with a provided message. +func NewFrom[V string | []byte](msg V) *T { return &T{Message: []byte(msg)} } + +// Label returns the label of a NOTICE envelope. +func (en *T) Label() string { return L } + +// Write the NOTICE T to a provided io.Writer. +func (en *T) Write(w io.Writer) (err error) { + _, err = w.Write(en.Marshal(nil)) + return +} + +// Marshal a NOTICE envelope in minified JSON into an noticeenvelope.T, +// appending to a provided destination slice. Note that this ensures correct +// string escaping on the Reason field. +func (en *T) Marshal(dst []byte) (b []byte) { + var err error + _ = err + b = dst + b = envelopes.Marshal( + b, L, + func(bst []byte) (o []byte) { + o = bst + o = append(o, '"') + o = text.NostrEscape(o, en.Message) + o = append(o, '"') + return + }, + ) + return +} + +// Unmarshal a noticeenvelope.T from minified JSON, returning the remainder +// after the end of the envelope. Note that this ensures the Reason string is +// correctly unescaped by NIP-01 escaping rules. +func (en *T) Unmarshal(b []byte) (r []byte, err error) { + r = b + if en.Message, r, err = text.UnmarshalQuoted(r); chk.E(err) { + return + } + if r, err = envelopes.SkipToTheEnd(r); chk.E(err) { + return + } + return +} + +// Parse reads a NOTICE envelope in minified JSON into a newly allocated +// noticeenvelope.T. +func Parse(b []byte) (t *T, rem []byte, err error) { + t = New() + if rem, err = t.Unmarshal(b); chk.E(err) { + return + } + return +} diff --git a/pkg/encoders/envelopes/noticeenvelope/noticeenvelope_test.go b/pkg/encoders/envelopes/noticeenvelope/noticeenvelope_test.go new file mode 100644 index 0000000..fb907e5 --- /dev/null +++ b/pkg/encoders/envelopes/noticeenvelope/noticeenvelope_test.go @@ -0,0 +1,66 @@ +package noticeenvelope + +import ( + "testing" + + "lol.mleku.dev/chk" + "next.orly.dev/pkg/encoders/envelopes" + "next.orly.dev/pkg/encoders/envelopes/messages" + "next.orly.dev/pkg/utils" +) + +func TestMarshalUnmarshal(t *testing.T) { + var err error + rb, rb1, rb2 := make([]byte, 0, 65535), make([]byte, 0, 65535), make( + []byte, 0, 65535, + ) + for _ = range 1000 { + req := NewFrom(messages.RandomMessage()) + rb = req.Marshal(rb) + rb1 = rb1[:len(rb)] + copy(rb1, rb) + var rem []byte + var l string + if l, rb, err = envelopes.Identify(rb); chk.E(err) { + t.Fatal(err) + } + if l != L { + t.Fatalf("invalid sentinel %s, expect %s", l, L) + } + req2 := New() + if rem, err = req2.Unmarshal(rb); chk.E(err) { + t.Fatal(err) + } + // log.I.Ln(req2.ID) + if len(rem) > 0 { + t.Fatalf( + "unmarshal failed, remainder\n%d %s", + len(rem), rem, + ) + } + rb2 = req2.Marshal(rb2) + if !utils.FastEqual(rb1, rb2) { + if len(rb1) != len(rb2) { + t.Fatalf( + "unmarshal failed, different lengths\n%d %s\n%d %s\n", + len(rb1), rb1, len(rb2), rb2, + ) + } + for i := range rb1 { + if rb1[i] != rb2[i] { + t.Fatalf( + "unmarshal failed, difference at position %d\n"+ + "%d %s\n%s\n%d %s\n%s\n", + i, len(rb1), rb1[:i], rb1[i:], len(rb2), rb2[:i], + rb2[i:], + ) + } + } + t.Fatalf( + "unmarshal failed\n%d %s\n%d %s\n", + len(rb1), rb1, len(rb2), rb2, + ) + } + rb, rb1, rb2 = rb[:0], rb1[:0], rb2[:0] + } +} diff --git a/pkg/encoders/envelopes/okenvelope/okenvelope.go b/pkg/encoders/envelopes/okenvelope/okenvelope.go new file mode 100644 index 0000000..d4f4a23 --- /dev/null +++ b/pkg/encoders/envelopes/okenvelope/okenvelope.go @@ -0,0 +1,133 @@ +// Package okenvelope is a codec for the OK message, which is an acknowledgement +// for an EVENT eventenvelope.Submission, containing true/false and if false a +// message with a machine readable error type as found in the messages package. +package okenvelope + +import ( + "io" + + "lol.mleku.dev/chk" + "lol.mleku.dev/errorf" + "lol.mleku.dev/log" + "next.orly.dev/pkg/crypto/sha256" + "next.orly.dev/pkg/encoders/envelopes" + "next.orly.dev/pkg/encoders/hex" + "next.orly.dev/pkg/encoders/text" + "next.orly.dev/pkg/interfaces/codec" +) + +// L is the label associated with this type of codec.Envelope. +const L = "OK" + +// T is an OK envelope, used to signal acceptance or rejection, with a reason, +// to an eventenvelope.Submission. +type T struct { + EventID []byte + OK bool + Reason []byte +} + +var _ codec.Envelope = (*T)(nil) + +// New creates a new empty OK T. +func New() *T { return &T{} } + +// NewFrom creates a new okenvelope.T with a string for the subscription.Id and +// the optional reason. +func NewFrom[V string | []byte](eid V, ok bool, msg ...V) *T { + var m []byte + if len(msg) > 0 { + m = []byte(msg[0]) + } + if len(eid) != sha256.Size { + log.W.F( + "event ID unexpected length, expect %d got %d", + len(eid), sha256.Size, + ) + } + return &T{EventID: []byte(eid), OK: ok, Reason: m} +} + +// Label returns the label of an okenvelope.T. +func (en *T) Label() string { return L } + +// ReasonString returns the Reason in the form of a string. +func (en *T) ReasonString() string { return string(en.Reason) } + +// Write the okenvelope.T to a provided io.Writer. +func (en *T) Write(w io.Writer) (err error) { + _, err = w.Write(en.Marshal(nil)) + return +} + +// Marshal a okenvelope.T from minified JSON, appending to a provided +// destination slice. Note that this ensures correct string escaping on the +// subscription.Id and Reason fields. +func (en *T) Marshal(dst []byte) (b []byte) { + var err error + _ = err + b = dst + b = envelopes.Marshal( + b, L, + func(bst []byte) (o []byte) { + o = bst + o = append(o, '"') + o = hex.EncAppend(o, en.EventID) + o = append(o, '"') + o = append(o, ',') + o = text.MarshalBool(o, en.OK) + o = append(o, ',') + o = append(o, '"') + o = text.NostrEscape(o, en.Reason) + o = append(o, '"') + return + }, + ) + return +} + +// Unmarshal a okenvelope.T from minified JSON, returning the remainder after +// the end of the envelope. Note that this ensures the Reason and +// subscription.Id strings are correctly unescaped by NIP-01 escaping rules. +func (en *T) Unmarshal(b []byte) (r []byte, err error) { + r = b + var idBytes []byte + // Parse event id as quoted hex (NIP-20 compliant) + if idBytes, r, err = text.UnmarshalHex(r); err != nil { + return + } + if len(idBytes) != sha256.Size { + err = errorf.E( + "invalid size for ID, require %d got %d", + sha256.Size, len(idBytes), + ) + return + } + en.EventID = idBytes + if r, err = text.Comma(r); chk.E(err) { + return + } + if r, en.OK, err = text.UnmarshalBool(r); chk.E(err) { + return + } + if r, err = text.Comma(r); chk.E(err) { + return + } + if en.Reason, r, err = text.UnmarshalQuoted(r); chk.E(err) { + return + } + if r, err = envelopes.SkipToTheEnd(r); chk.E(err) { + return + } + return +} + +// Parse reads a OK envelope in minified JSON into a newly allocated +// okenvelope.T. +func Parse(b []byte) (t *T, rem []byte, err error) { + t = New() + if rem, err = t.Unmarshal(b); chk.E(err) { + return + } + return +} diff --git a/pkg/encoders/envelopes/okenvelope/okenvelope_test.go b/pkg/encoders/envelopes/okenvelope/okenvelope_test.go new file mode 100644 index 0000000..276b4fb --- /dev/null +++ b/pkg/encoders/envelopes/okenvelope/okenvelope_test.go @@ -0,0 +1,71 @@ +package okenvelope + +import ( + "testing" + + "lol.mleku.dev/chk" + "lukechampine.com/frand" + "next.orly.dev/pkg/crypto/sha256" + "next.orly.dev/pkg/encoders/envelopes" + "next.orly.dev/pkg/encoders/envelopes/messages" + "next.orly.dev/pkg/utils" +) + +func TestMarshalUnmarshal(t *testing.T) { + var err error + rb, rb1, rb2 := make([]byte, 0, 65535), make([]byte, 0, 65535), make( + []byte, 0, 65535, + ) + for i := range 1000 { + var randMsg []byte + ok := i%2 == 1 + if !ok { + randMsg = messages.RandomMessage() + } + req := NewFrom(frand.Bytes(sha256.Size), ok, randMsg) + rb = req.Marshal(rb) + rb1 = rb1[:len(rb)] + copy(rb1, rb) + var rem []byte + var l string + if l, rb, err = envelopes.Identify(rb); chk.E(err) { + t.Fatal(err) + } + if l != L { + t.Fatalf("invalid sentinel %s, expect %s", l, L) + } + req2 := New() + if rem, err = req2.Unmarshal(rb); chk.E(err) { + t.Fatal(err) + } + if len(rem) > 0 { + t.Fatalf( + "unmarshal failed, remainder\n%d %s", + len(rem), rem, + ) + } + rb2 = req2.Marshal(rb2) + if !utils.FastEqual(rb1, rb2) { + if len(rb1) != len(rb2) { + t.Fatalf( + "unmarshal failed, different lengths\n%d %s\n%d %s\n", + len(rb1), rb1, len(rb2), rb2, + ) + } + for i := range rb1 { + if rb1[i] != rb2[i] { + t.Fatalf( + "unmarshal failed, difference at position %d\n%d %s\n%s\n%d %s\n%s\n", + i, len(rb1), rb1[:i], rb1[i:], len(rb2), rb2[:i], + rb2[i:], + ) + } + } + t.Fatalf( + "unmarshal failed\n%d %s\n%d %s\n", + len(rb1), rb1, len(rb2), rb2, + ) + } + rb, rb1, rb2 = rb[:0], rb1[:0], rb2[:0] + } +}