diff --git a/directories/readme.adoc b/directories/readme.adoc new file mode 100644 index 0000000..057e5f6 --- /dev/null +++ b/directories/readme.adoc @@ -0,0 +1,3 @@ += directories + +repos are relays that store data about users, including access privilege lists for other relays diff --git a/go.mod b/go.mod index a5d923a..29d7246 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/fatih/color v1.18.0 go.uber.org/atomic v1.11.0 - golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c + golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 lukechampine.com/frand v1.5.1 ) @@ -14,5 +14,5 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/stretchr/testify v1.10.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.30.0 // indirect ) diff --git a/go.sum b/go.sum index e764eb4..4cebe08 100644 --- a/go.sum +++ b/go.sum @@ -14,9 +14,13 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc= golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 h1:qNgPs5exUA+G0C96DrPwNrvLSj7GT/9D+3WMWUcUg34= +golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w= diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go new file mode 100644 index 0000000..f6d905f --- /dev/null +++ b/pkg/auth/auth.go @@ -0,0 +1,47 @@ +package auth + +import ( + "protocol.realy.lol/pkg/codec" + "protocol.realy.lol/pkg/decimal" + "protocol.realy.lol/pkg/pubkey" + "protocol.realy.lol/pkg/signature" + "protocol.realy.lol/pkg/url" +) + +type Message struct { + Payload codec.C + RequestURL *url.U + Timestamp *decimal.T + PubKey *pubkey.P + Signature *signature.S +} + +func SignMessage(msg *Message) (m *Message, err error) { + + return +} + +func (m *Message) Marshal(d []byte) (r []byte, err error) { + r = d + if r, err = m.Payload.Marshal(d); chk.E(err) { + return + } + if r, err = m.RequestURL.Marshal(d); chk.E(err) { + return + } + if r, err = m.Timestamp.Marshal(d); chk.E(err) { + return + } + if r, err = m.PubKey.Marshal(d); chk.E(err) { + return + } + if r, err = m.Signature.Marshal(d); chk.E(err) { + return + } + return +} + +func (m *Message) Unmarshal(d []byte) (r []byte, err error) { + + return +} diff --git a/pkg/auth/auth_test.go b/pkg/auth/auth_test.go new file mode 100644 index 0000000..8832b06 --- /dev/null +++ b/pkg/auth/auth_test.go @@ -0,0 +1 @@ +package auth diff --git a/pkg/timestamp/log.go b/pkg/auth/log.go similarity index 86% rename from pkg/timestamp/log.go rename to pkg/auth/log.go index 2fb68f2..6adcb19 100644 --- a/pkg/timestamp/log.go +++ b/pkg/auth/log.go @@ -1,4 +1,4 @@ -package timestamp +package auth import ( "protocol.realy.lol/pkg/lol" diff --git a/pkg/codec/codec.go b/pkg/codec/codec.go index a63ecd5..6af0158 100644 --- a/pkg/codec/codec.go +++ b/pkg/codec/codec.go @@ -6,8 +6,8 @@ package codec type C interface { // Marshal data by appending it to the provided destination, and return the // resultant slice. - Marshal(dst []byte) (result []byte, err error) + Marshal(dst []byte) (r []byte, err error) // Unmarshal the next expected data element from the provided slice and return // the remainder after the expected separator. - Unmarshal(data []byte) (rem []byte, err error) + Unmarshal(data []byte) (r []byte, err error) } diff --git a/pkg/content/content.go b/pkg/content/content.go index dd1791a..c1db688 100644 --- a/pkg/content/content.go +++ b/pkg/content/content.go @@ -2,53 +2,65 @@ package content import ( "bytes" + "io" + + "protocol.realy.lol/pkg/decimal" ) -// C is raw content bytes of a message. This can contain anything but when it is -// unmarshalled it is assumed that the last line (content between the second -// last and last line break) is not part of the content, as this is where the -// signature is placed. -// -// The only guaranteed property of an encoded content.C is that it has two -// newline characters, one at the very end, and a second one before it that -// demarcates the end of the actual content. It can be entirely binary and mess -// up a terminal to render the unsanitized possible control characters. +// C is raw content bytes of a message. type C struct{ Content []byte } // Marshal just writes the provided data with a `content:\n` prefix and adds a // terminal newline. -func (c *C) Marshal(dst []byte) (result []byte, err error) { - result = append(append(append(dst, "content:\n"...), c.Content...), '\n') +func (c *C) Marshal(d []byte) (r []byte, err error) { + r = append(d, "content:"...) + if r, err = decimal.New(len(c.Content)).Marshal(r); chk.E(err) { + return + } + r = append(r, '\n') + // log.I.S(r) + r = append(r, c.Content...) + r = append(r, '\n') + // log.I.S(r) return } -var Prefix = "content:\n" +var Prefix = "content:" -// Unmarshal expects the `content:\n` prefix and stops at the second last +// Unmarshal expects the `content:\n` prefix and stops at the second last // newline. The data between the second last and last newline in the data is -// assumed to be a signature but it could be anything in another use case. -func (c *C) Unmarshal(data []byte) (rem []byte, err error) { - if !bytes.HasPrefix(data, []byte("content:\n")) { - err = errorf.E("content prefix `content:\\n' not found: '%s'", data[:len(Prefix)+1]) +// assumed to be a signature, but it could be anything in another use case. +// +// It is necessary that any non-content elements after the content must be +// parsed before returning to the content, because this is a +func (c *C) Unmarshal(d []byte) (r []byte, err error) { + if !bytes.HasPrefix(d, []byte(Prefix)) { + err = errorf.E("content prefix `content:' not found: '%s'", d[:len(Prefix)]) return } // trim off the prefix. - data = data[len(Prefix):] - // check that there is a last newline. - if data[len(data)-1] != '\n' { - err = errorf.E("input data does not end with newline") + d = d[len(Prefix):] + l := decimal.New(0) + if d, err = l.Unmarshal(d); chk.E(err) { return } - // we start at the second last, previous to the terminal newline byte. - lastPos := len(data) - 2 - for ; lastPos >= len(Prefix); lastPos-- { - // the content ends at the byte before the second last newline byte. - if data[lastPos] == '\n' { - break - } + // and then there must be a newline + if d[0] != '\n' { + err = errorf.E("must be newline after content::\n%n", d) + return } - c.Content = data[:lastPos] - // return the remainder after the content-terminal newline byte. - rem = data[lastPos+1:] + d = d[1:] + // log.I.S(l.Uint64(), d) + if len(d) < int(l.N) { + err = io.EOF + return + } + c.Content = d[:l.N] + r = d[l.N:] + if r[0] != '\n' { + err = errorf.E("must be newline after content:\\n, got '%s' %x", c.Content[len(c.Content)-1]) + return + } + r = r[1:] return } diff --git a/pkg/content/content_test.go b/pkg/content/content_test.go index f3bbe40..2c3447d 100644 --- a/pkg/content/content_test.go +++ b/pkg/content/content_test.go @@ -19,19 +19,17 @@ func TestC_Marshal_Unmarshal(t *testing.T) { if res, err = c1.Marshal(nil); chk.E(err) { t.Fatal(err) } - // append a fake zero length signature - res = append(res, '\n') c2 := new(C) var rem []byte if rem, err = c2.Unmarshal(res); chk.E(err) { t.Fatal(err) } if !bytes.Equal(c1.Content, c2.Content) { - log.I.S(c1, c2) + log.I.S(c1.Content, c2.Content) t.Fatal("content not equal") } - if !bytes.Equal(rem, []byte{'\n'}) { + if len(rem) > 0 { log.I.S(rem) - t.Fatalf("remainder not found") + t.Fatalf("unexpected remaining bytes: '%0x'", rem) } } diff --git a/pkg/timestamp/base10k.txt b/pkg/decimal/base10k.txt similarity index 100% rename from pkg/timestamp/base10k.txt rename to pkg/decimal/base10k.txt diff --git a/pkg/timestamp/timestamp.go b/pkg/decimal/decimal.go similarity index 84% rename from pkg/timestamp/timestamp.go rename to pkg/decimal/decimal.go index ce79fef..5488fe6 100644 --- a/pkg/timestamp/timestamp.go +++ b/pkg/decimal/decimal.go @@ -1,4 +1,4 @@ -package timestamp +package decimal import ( _ "embed" @@ -26,7 +26,7 @@ func (n *T) Int64() int64 { return int64(n.N) } func (n *T) Uint16() uint16 { return uint16(n.N) } var powers = []*T{ - {1}, + {base / base}, {base}, {base * base}, {base * base * base}, @@ -36,15 +36,15 @@ var powers = []*T{ const zero = '0' const nine = '9' -func (n *T) Marshal(dst []byte) (b []byte, err error) { +func (n *T) Marshal(d []byte) (r []byte, err error) { if n == nil { err = errorf.E("cannot marshal nil timestamp") return } nn := n.N - b = dst + r = d if n.N == 0 { - b = append(b, '0') + r = append(r, '0') return } var i int @@ -67,7 +67,7 @@ func (n *T) Marshal(dst []byte) (b []byte, err error) { } } } - b = append(b, bb...) + r = append(r, bb...) n.N = n.N - q*powers[k].N } n.N = nn @@ -81,19 +81,19 @@ func (n *T) Marshal(dst []byte) (b []byte, err error) { // generated JSON integers with leading zeroes. Until this is disproven, this is the fastest way // to read a positive json integer, and a leading zero is decoded as a zero, and the remainder // returned. -func (n *T) Unmarshal(b []byte) (r []byte, err error) { - if len(b) < 1 { +func (n *T) Unmarshal(d []byte) (r []byte, err error) { + if len(d) < 1 { err = errorf.E("zero length number") return } var sLen int - if b[0] == zero { - r = b[1:] + if d[0] == zero { + r = d[1:] n.N = 0 return } // count the digits - for ; sLen < len(b) && b[sLen] >= zero && b[sLen] <= nine && b[sLen] != ','; sLen++ { + for ; sLen < len(d) && d[sLen] >= zero && d[sLen] <= nine && d[sLen] != ','; sLen++ { } if sLen == 0 { err = errorf.E("zero length number") @@ -104,9 +104,9 @@ func (n *T) Unmarshal(b []byte) (r []byte, err error) { return } // the length of the string found - r = b[sLen:] - b = b[:sLen] - for _, ch := range b { + r = d[sLen:] + d = d[:sLen] + for _, ch := range d { ch -= zero n.N = n.N*10 + uint64(ch) } diff --git a/pkg/timestamp/timestamp_test.go b/pkg/decimal/decimal_test.go similarity index 98% rename from pkg/timestamp/timestamp_test.go rename to pkg/decimal/decimal_test.go index 4fe1a5f..ac39132 100644 --- a/pkg/timestamp/timestamp_test.go +++ b/pkg/decimal/decimal_test.go @@ -1,4 +1,4 @@ -package timestamp +package decimal import ( "math" diff --git a/pkg/timestamp/gen/log.go b/pkg/decimal/gen/log.go similarity index 100% rename from pkg/timestamp/gen/log.go rename to pkg/decimal/gen/log.go diff --git a/pkg/timestamp/gen/pregen.go b/pkg/decimal/gen/pregen.go similarity index 100% rename from pkg/timestamp/gen/pregen.go rename to pkg/decimal/gen/pregen.go diff --git a/pkg/decimal/log.go b/pkg/decimal/log.go new file mode 100644 index 0000000..eab1694 --- /dev/null +++ b/pkg/decimal/log.go @@ -0,0 +1,9 @@ +package decimal + +import ( + "protocol.realy.lol/pkg/lol" +) + +var ( + log, chk, errorf = lol.Main.Log, lol.Main.Check, lol.Main.Errorf +) diff --git a/pkg/event/event.go b/pkg/event/event.go index ec876ae..107a04c 100644 --- a/pkg/event/event.go +++ b/pkg/event/event.go @@ -2,18 +2,72 @@ package event import ( "protocol.realy.lol/pkg/content" + "protocol.realy.lol/pkg/decimal" "protocol.realy.lol/pkg/event/types" "protocol.realy.lol/pkg/pubkey" "protocol.realy.lol/pkg/signature" "protocol.realy.lol/pkg/tags" - "protocol.realy.lol/pkg/timestamp" ) -type Event struct { +type E struct { Type *types.T Pubkey *pubkey.P - Timestamp *timestamp.T + Timestamp *decimal.T Tags *tags.T Content *content.C Signature *signature.S } + +// New creates a new event with some typical data already filled. This should be +// populated by some kind of editor. +// +// Simplest form of this would be to create a temporary file, open user's +// default editor with the event already populated, they enter the content +// field's message, and then after closing the editor it scans the text for e: +// and p: event and pubkey references and maybe #hashtags, updates the +// timestamp, and then signs it with a signing key, wraps in an event publish +// request, stamps and signs it and then pushes it to a configured relay +// address. +// +// Other more complex edit flows could be created but this one is for a simple +// flow as described. REALY events are text, and it is simple to make them +// literally edit as simple text files. REALY is natively text files, and the +// first composition client should just be a text editor. +func New(pk []byte, typ string) (ev *E, err error) { + var p *pubkey.P + p, err = pubkey.New(pk) + ev = &E{ + Type: types.New(typ), + Pubkey: p, + Timestamp: decimal.Now(), + } + return +} + +func (e *E) Marshal(d []byte) (r []byte, err error) { + r = d + if r, err = e.Type.Marshal(d); chk.E(err) { + return + } + if r, err = e.Pubkey.Marshal(d); chk.E(err) { + return + } + if r, err = e.Timestamp.Marshal(d); chk.E(err) { + return + } + if r, err = e.Tags.Marshal(d); chk.E(err) { + return + } + if r, err = e.Content.Marshal(d); chk.E(err) { + return + } + if r, err = e.Signature.Marshal(d); chk.E(err) { + return + } + return +} + +func (e *E) Unmarshal(data []byte) (r []byte, err error) { + + return +} diff --git a/pkg/event/event_test.go b/pkg/event/event_test.go new file mode 100644 index 0000000..00f409e --- /dev/null +++ b/pkg/event/event_test.go @@ -0,0 +1,9 @@ +package event + +import ( + "testing" +) + +func TestE_Marshal_Unmarshal(t *testing.T) { + +} diff --git a/pkg/event/types/types.go b/pkg/event/types/types.go index f7c52f8..9fea202 100644 --- a/pkg/event/types/types.go +++ b/pkg/event/types/types.go @@ -8,38 +8,38 @@ import ( // A T is a type descriptor, that is terminated by a newline. type T struct{ t []byte } -func New[V ~[]byte | ~string](t V) T { return T{[]byte(t)} } +func New[V ~[]byte | ~string](t V) *T { return &T{[]byte(t)} } func (t *T) Equal(t2 *T) bool { return bytes.Equal(t.t, t2.t) } // Marshal append the T to a slice and appends a terminal newline, and returns // the result. -func (t *T) Marshal(dst []byte) (result []byte, err error) { +func (t *T) Marshal(d []byte) (r []byte, err error) { if t == nil { return } - result = append(append(dst, t.t...), '\n') + r = append(append(d, t.t...), '\n') return } // Unmarshal expects an identifier followed by a newline. If the buffer ends // without a newline an EOF is returned. -func (t *T) Unmarshal(data []byte) (rem []byte, err error) { - rem = data +func (t *T) Unmarshal(d []byte) (r []byte, err error) { + r = d if t == nil { err = errorf.E("can't unmarshal into nil types.T") return } - if len(rem) < 2 { + if len(r) < 2 { err = errorf.E("can't unmarshal nothing") return } - for i := range rem { - if rem[i] == '\n' { + for i := range r { + if r[i] == '\n' { // write read data up to the newline and return the remainder after // the newline. - t.t = rem[:i] - rem = rem[i+1:] + t.t = r[:i] + r = r[i+1:] return } } diff --git a/pkg/id/id.go b/pkg/id/id.go index ff70987..e569812 100644 --- a/pkg/id/id.go +++ b/pkg/id/id.go @@ -21,8 +21,8 @@ func New(id []byte) (p *P, err error) { return } -func (p *P) Marshal(dst []byte) (result []byte, err error) { - result = dst +func (p *P) Marshal(d []byte) (r []byte, err error) { + r = d if p == nil || p.b == nil || len(p.b) == 0 { err = errorf.E("nil/zero length pubkey") return @@ -32,7 +32,7 @@ func (p *P) Marshal(dst []byte) (result []byte, err error) { len(p.b), ed25519.PublicKeySize, p.b) return } - buf := bytes.NewBuffer(result) + buf := bytes.NewBuffer(r) w := base64.NewEncoder(base64.RawURLEncoding, buf) if _, err = w.Write(p.b); chk.E(err) { return @@ -40,32 +40,32 @@ func (p *P) Marshal(dst []byte) (result []byte, err error) { if err = w.Close(); chk.E(err) { return } - result = append(buf.Bytes(), '\n') + r = append(buf.Bytes(), '\n') return } -func (p *P) Unmarshal(data []byte) (rem []byte, err error) { - rem = data +func (p *P) Unmarshal(data []byte) (r []byte, err error) { + r = data if p == nil { err = errorf.E("can't unmarshal into nil types.T") return } - if len(rem) < 2 { + if len(r) < 2 { err = errorf.E("can't unmarshal nothing") return } - for i := range rem { - if rem[i] == '\n' { + for i := range r { + if r[i] == '\n' { if i != Len { err = errorf.E("invalid encoded pubkey length %d; require %d '%0x'", - i, Len, rem[:i]) + i, Len, r[:i]) return } p.b = make([]byte, ed25519.PublicKeySize) - if _, err = base64.RawURLEncoding.Decode(p.b, rem[:i]); chk.E(err) { + if _, err = base64.RawURLEncoding.Decode(p.b, r[:i]); chk.E(err) { return } - rem = rem[i+1:] + r = r[i+1:] return } } diff --git a/pkg/pubkey/pubkey.go b/pkg/pubkey/pubkey.go index 6611d60..6c8c5e8 100644 --- a/pkg/pubkey/pubkey.go +++ b/pkg/pubkey/pubkey.go @@ -21,8 +21,8 @@ func New(pk []byte) (p *P, err error) { return } -func (p *P) Marshal(dst []byte) (result []byte, err error) { - result = dst +func (p *P) Marshal(d []byte) (r []byte, err error) { + r = d if p == nil || p.PublicKey == nil || len(p.PublicKey) == 0 { err = errorf.E("nil/zero length pubkey") return @@ -32,7 +32,7 @@ func (p *P) Marshal(dst []byte) (result []byte, err error) { len(p.PublicKey), ed25519.PublicKeySize, p.PublicKey) return } - buf := bytes.NewBuffer(result) + buf := bytes.NewBuffer(r) w := base64.NewEncoder(base64.RawURLEncoding, buf) if _, err = w.Write(p.PublicKey); chk.E(err) { return @@ -40,32 +40,32 @@ func (p *P) Marshal(dst []byte) (result []byte, err error) { if err = w.Close(); chk.E(err) { return } - result = append(buf.Bytes(), '\n') + r = append(buf.Bytes(), '\n') return } -func (p *P) Unmarshal(data []byte) (rem []byte, err error) { - rem = data +func (p *P) Unmarshal(d []byte) (r []byte, err error) { + r = d if p == nil { err = errorf.E("can't unmarshal into nil types.T") return } - if len(rem) < 2 { + if len(r) < 2 { err = errorf.E("can't unmarshal nothing") return } - for i := range rem { - if rem[i] == '\n' { + for i := range r { + if r[i] == '\n' { if i != Len { err = errorf.E("invalid encoded pubkey length %d; require %d '%0x'", - i, Len, rem[:i]) + i, Len, r[:i]) return } p.PublicKey = make([]byte, ed25519.PublicKeySize) - if _, err = base64.RawURLEncoding.Decode(p.PublicKey, rem[:i]); chk.E(err) { + if _, err = base64.RawURLEncoding.Decode(p.PublicKey, r[:i]); chk.E(err) { return } - rem = rem[i+1:] + r = r[i+1:] return } } diff --git a/pkg/signature/signature.go b/pkg/signature/signature.go index 60e24fa..3d39f82 100644 --- a/pkg/signature/signature.go +++ b/pkg/signature/signature.go @@ -21,8 +21,16 @@ func New(sig []byte) (p *S, err error) { return } -func (p *S) Marshal(dst []byte) (result []byte, err error) { - result = dst +func Sign(msg []byte, sec ed25519.PrivateKey) (sig []byte, err error) { + return sec.Sign(nil, msg, nil) +} + +func Verify(msg []byte, pub ed25519.PublicKey, sig []byte) (ok bool) { + return ed25519.Verify(pub, msg, sig) +} + +func (p *S) Marshal(d []byte) (r []byte, err error) { + r = d if p == nil || p.Signature == nil || len(p.Signature) == 0 { err = errorf.E("nil/zero length signature") return @@ -32,7 +40,7 @@ func (p *S) Marshal(dst []byte) (result []byte, err error) { len(p.Signature), ed25519.SignatureSize, p.Signature) return } - buf := bytes.NewBuffer(result) + buf := bytes.NewBuffer(r) w := base64.NewEncoder(base64.RawURLEncoding, buf) if _, err = w.Write(p.Signature); chk.E(err) { return @@ -40,32 +48,32 @@ func (p *S) Marshal(dst []byte) (result []byte, err error) { if err = w.Close(); chk.E(err) { return } - result = append(buf.Bytes(), '\n') + r = append(buf.Bytes(), '\n') return } -func (p *S) Unmarshal(data []byte) (rem []byte, err error) { - rem = data +func (p *S) Unmarshal(d []byte) (r []byte, err error) { + r = d if p == nil { err = errorf.E("can't unmarshal into nil types.T") return } - if len(rem) < 2 { + if len(r) < 2 { err = errorf.E("can't unmarshal nothing") return } - for i := range rem { - if rem[i] == '\n' { + for i := range r { + if r[i] == '\n' { if i != Len { err = errorf.E("invalid encoded signature length %d; require %d '%0x'", - i, Len, rem[:i]) + i, Len, r[:i]) return } p.Signature = make([]byte, ed25519.SignatureSize) - if _, err = base64.RawURLEncoding.Decode(p.Signature, rem[:i]); chk.E(err) { + if _, err = base64.RawURLEncoding.Decode(p.Signature, r[:i]); chk.E(err) { return } - rem = rem[i+1:] + r = r[i+1:] return } } diff --git a/pkg/tag/tag.go b/pkg/tag/tag.go index 3e36876..ef6af17 100644 --- a/pkg/tag/tag.go +++ b/pkg/tag/tag.go @@ -77,32 +77,32 @@ func ValidateField[V ~[]byte | ~string](f V, i int) (k []byte, err error) { return } -func (t *T) Marshal(dst []byte) (result []byte, err error) { - result = dst +func (t *T) Marshal(d []byte) (r []byte, err error) { + r = d if len(t.fields) == 0 { return } for i, field := range t.fields { - result = append(result, field...) + r = append(r, field...) if i == 0 { - result = append(result, ':') + r = append(r, ':') } else if i == len(t.fields)-1 { - result = append(result, '\n') + r = append(r, '\n') } else { - result = append(result, ';') + r = append(r, ';') } } return } -func (t *T) Unmarshal(data []byte) (rem []byte, err error) { +func (t *T) Unmarshal(d []byte) (r []byte, err error) { var i int var v byte var dat []byte // first find the end - for i, v = range data { + for i, v = range d { if v == '\n' { - dat, rem = data[:i], data[i+1:] + dat, r = d[:i], d[i+1:] break } } diff --git a/readme.adoc b/readme.adoc index fb74cac..2ea4c9d 100644 --- a/readme.adoc +++ b/readme.adoc @@ -10,6 +10,7 @@ zap mleku: ⚡️mleku@getalby.com Inspired by the event bus architecture of link:https://github.com/nostr-protocol[nostr] but redesigned to avoid the serious deficiencies of that protocol for both developers and users. * link:./relays/readme.adoc[reference relays] +* link:./repos/readme.adoc[reference repos] * link:./clients/readme.adoc[reference clients] * link:./pkg/readme.adoc[_GO⌯_ libraries] @@ -241,12 +242,14 @@ Every REALY protocol should be simple and precise, and use HTTP for request/resp The list of protocols below can be expanded to add new categories. The design should be as general as possible for each to isolate the application features from the relay processing cleanly. -=== `store`, `replace` and `relay` Requests +=== Publication + +=== store, update and relay store\n - replace:\n + update:\n relay:\n @@ -267,12 +270,14 @@ Events that are returned have the `:\n` as the first There is four basic types of queries in REALY, derived from the `nostr` design, but refined and separated into distinct, small API calls. -=== `events` Query +=== Queries + +==== events A key concept in REALY protocol is minimising the footprint of each API call. Thus, a primary query type is the simple request for a list of events by their ID hash: -==== Request +===== Request .events request [options="header"] @@ -289,7 +294,7 @@ Normally clients will gather a potentially longer list of events and then send E The results are returned as a series as follows, for each item returned: -==== Response +===== Response .events response [options="header"] @@ -299,11 +304,11 @@ The results are returned as a series as follows, for each item returned: |`\n`| the full event text as described previously |==== -=== `filter` Query +==== filter A filter has one or more of the fields listed below, and headed with `filter`: -==== Request +===== Request .filter request [options="header"] @@ -321,9 +326,9 @@ A filter has one or more of the fields listed below, and headed with `filter`: The response message is simply a list of the matching events IDs, which are expected to be in reverse chronological order: -==== Response +===== Response -.filter request +.filter response [options="header"] |==== | Message | Description @@ -332,22 +337,60 @@ The response message is simply a list of the matching events IDs, which are expe |`...` | ...any number of events further. |==== -=== `subscribe` Query +==== subscribe -This is identical to `filter` as above but establishes a websocket connection to return the results, and each new result is sent in a single message over the websocket as it arrives from a `store` or `relay` message sent to the relay. +`subscribe` means to request to be sent events that match a filter, from the moment the request is received. Mixing queries and subscriptions is a bad idea because it makes it difficult to specify the expected behaviour from a relay, or client. Thus, a subset of the `filter` is used. The subscription ends when the client sends `unsubscribe` message. + +.subscribe request +[options="header"] +|==== +| Message | Description +|`subscribe:\n` | the ID is for the use of the client to distinguish between multiple subscriptions on one socket, there can be more than one. +|`types:;;...\n` | these should be the same as the ones that appear in events, and match on the prefix so subtypes, eg `note/text` and `note/html` will both match on `note`. +|`pubkeys:;;...\n` | list of pubkeys to only return results from +|`tags:\n` | these end with a second newline +|`:[;...]\n` | only the value can be searched for, and must be semicolon separated for multiple matches +|`...` | several tags can be present, they will act as OR +|`\n` | tags end with a second newline +|==== + +NOTE: **There is no timestamp field in a `subscribe`.** + +After a subscribe request the relay will send an acknowledgement: + +.subscribed response +[options="header"] +|==== +| Message | Description +|`subscribed:\n` | +|==== + +To close a subscription the client sends an `unsubscribe`: + +.unsubscribe request +[options="header"] +|==== +| Message | Description +|`unsubscribe:\n` | +|==== -A key distinction in this form is the `subscribe\n` can be followed by nothing, which will implicitly indicate to simply return all new event IDs that arrive from that moment forwards, in accordance with other constraints such as permission IMPORTANT: Direct messages, for example, are privileged and can only be sent in response to a query or subscription signed with one of the keys appearing in the message (author or recipient/s) -An empty filter is not valid for a `filter`, a full event dump could instead be a separate API as this is an intensive operation that should be restricted to administrators. -For this reason an empty `subscribe` is implicitly "from now". +The `subscribe` query streams back results containing just the event ID hash, in the following message: + +.subscription response +[options="header"] +|==== +| Message | Description +|`subscription::\n` | +|==== + -The `subscribe` query streams back results containing just the event ID hash. The client can then send an `events` query to actually fetch the data. This enables collecting a list and indicating the count without consuming the bandwidth for it until the view is opened. -=== `fulltext` Query +==== `fulltext` Query A fulltext query is just `fulltext:` followed by a series of space separated tokens if the event store has a full text index, terminated with a newline. diff --git a/relays/readme.adoc b/relays/readme.adoc index 4314016..0106e7d 100644 --- a/relays/readme.adoc +++ b/relays/readme.adoc @@ -1,3 +1,3 @@ = relays -relay implementations for various subprotocols \ No newline at end of file +relays specialise in subscription and relaying, and do not store events diff --git a/repos/readme.adoc b/repos/readme.adoc index 4314016..a396cf4 100644 --- a/repos/readme.adoc +++ b/repos/readme.adoc @@ -1,3 +1,3 @@ -= relays += repos -relay implementations for various subprotocols \ No newline at end of file +repos are relays that store data