update drafts of several subdirectories and main readme.adoc

This commit is contained in:
2025-02-10 17:33:44 -01:06
parent e639428ab7
commit f166402702
25 changed files with 321 additions and 133 deletions

3
directories/readme.adoc Normal file
View File

@@ -0,0 +1,3 @@
= directories
repos are relays that store data about users, including access privilege lists for other relays

4
go.mod
View File

@@ -6,7 +6,7 @@ require (
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
go.uber.org/atomic v1.11.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 lukechampine.com/frand v1.5.1
) )
@@ -14,5 +14,5 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/stretchr/testify v1.10.0 // 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
) )

4
go.sum
View File

@@ -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= 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 h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w= lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w=

47
pkg/auth/auth.go Normal file
View File

@@ -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
}

1
pkg/auth/auth_test.go Normal file
View File

@@ -0,0 +1 @@
package auth

View File

@@ -1,4 +1,4 @@
package timestamp package auth
import ( import (
"protocol.realy.lol/pkg/lol" "protocol.realy.lol/pkg/lol"

View File

@@ -6,8 +6,8 @@ package codec
type C interface { type C interface {
// Marshal data by appending it to the provided destination, and return the // Marshal data by appending it to the provided destination, and return the
// resultant slice. // 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 // Unmarshal the next expected data element from the provided slice and return
// the remainder after the expected separator. // the remainder after the expected separator.
Unmarshal(data []byte) (rem []byte, err error) Unmarshal(data []byte) (r []byte, err error)
} }

View File

@@ -2,53 +2,65 @@ package content
import ( import (
"bytes" "bytes"
"io"
"protocol.realy.lol/pkg/decimal"
) )
// C is raw content bytes of a message. This can contain anything but when it is // C is raw content bytes of a message.
// 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.
type C struct{ Content []byte } type C struct{ Content []byte }
// Marshal just writes the provided data with a `content:\n` prefix and adds a // Marshal just writes the provided data with a `content:\n` prefix and adds a
// terminal newline. // terminal newline.
func (c *C) Marshal(dst []byte) (result []byte, err error) { func (c *C) Marshal(d []byte) (r []byte, err error) {
result = append(append(append(dst, "content:\n"...), c.Content...), '\n') 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 return
} }
var Prefix = "content:\n" var Prefix = "content:"
// Unmarshal expects the `content:\n` prefix and stops at the second last // Unmarshal expects the `content:<length>\n` prefix and stops at the second last
// newline. The data between the second last and last newline in the data is // 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. // 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")) { // It is necessary that any non-content elements after the content must be
err = errorf.E("content prefix `content:\\n' not found: '%s'", data[:len(Prefix)+1]) // 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 return
} }
// trim off the prefix. // trim off the prefix.
data = data[len(Prefix):] d = d[len(Prefix):]
// check that there is a last newline. l := decimal.New(0)
if data[len(data)-1] != '\n' { if d, err = l.Unmarshal(d); chk.E(err) {
err = errorf.E("input data does not end with newline")
return return
} }
// we start at the second last, previous to the terminal newline byte. // and then there must be a newline
lastPos := len(data) - 2 if d[0] != '\n' {
for ; lastPos >= len(Prefix); lastPos-- { err = errorf.E("must be newline after content:<length>:\n%n", d)
// the content ends at the byte before the second last newline byte. return
if data[lastPos] == '\n' {
break
}
} }
c.Content = data[:lastPos] d = d[1:]
// return the remainder after the content-terminal newline byte. // log.I.S(l.Uint64(), d)
rem = data[lastPos+1:] 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:<length>\\n, got '%s' %x", c.Content[len(c.Content)-1])
return
}
r = r[1:]
return return
} }

View File

@@ -19,19 +19,17 @@ func TestC_Marshal_Unmarshal(t *testing.T) {
if res, err = c1.Marshal(nil); chk.E(err) { if res, err = c1.Marshal(nil); chk.E(err) {
t.Fatal(err) t.Fatal(err)
} }
// append a fake zero length signature
res = append(res, '\n')
c2 := new(C) c2 := new(C)
var rem []byte var rem []byte
if rem, err = c2.Unmarshal(res); chk.E(err) { if rem, err = c2.Unmarshal(res); chk.E(err) {
t.Fatal(err) t.Fatal(err)
} }
if !bytes.Equal(c1.Content, c2.Content) { if !bytes.Equal(c1.Content, c2.Content) {
log.I.S(c1, c2) log.I.S(c1.Content, c2.Content)
t.Fatal("content not equal") t.Fatal("content not equal")
} }
if !bytes.Equal(rem, []byte{'\n'}) { if len(rem) > 0 {
log.I.S(rem) log.I.S(rem)
t.Fatalf("remainder not found") t.Fatalf("unexpected remaining bytes: '%0x'", rem)
} }
} }

View File

@@ -1,4 +1,4 @@
package timestamp package decimal
import ( import (
_ "embed" _ "embed"
@@ -26,7 +26,7 @@ func (n *T) Int64() int64 { return int64(n.N) }
func (n *T) Uint16() uint16 { return uint16(n.N) } func (n *T) Uint16() uint16 { return uint16(n.N) }
var powers = []*T{ var powers = []*T{
{1}, {base / base},
{base}, {base},
{base * base}, {base * base},
{base * base * base}, {base * base * base},
@@ -36,15 +36,15 @@ var powers = []*T{
const zero = '0' const zero = '0'
const nine = '9' 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 { if n == nil {
err = errorf.E("cannot marshal nil timestamp") err = errorf.E("cannot marshal nil timestamp")
return return
} }
nn := n.N nn := n.N
b = dst r = d
if n.N == 0 { if n.N == 0 {
b = append(b, '0') r = append(r, '0')
return return
} }
var i int 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 = n.N - q*powers[k].N
} }
n.N = nn 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 // 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 // to read a positive json integer, and a leading zero is decoded as a zero, and the remainder
// returned. // returned.
func (n *T) Unmarshal(b []byte) (r []byte, err error) { func (n *T) Unmarshal(d []byte) (r []byte, err error) {
if len(b) < 1 { if len(d) < 1 {
err = errorf.E("zero length number") err = errorf.E("zero length number")
return return
} }
var sLen int var sLen int
if b[0] == zero { if d[0] == zero {
r = b[1:] r = d[1:]
n.N = 0 n.N = 0
return return
} }
// count the digits // 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 { if sLen == 0 {
err = errorf.E("zero length number") err = errorf.E("zero length number")
@@ -104,9 +104,9 @@ func (n *T) Unmarshal(b []byte) (r []byte, err error) {
return return
} }
// the length of the string found // the length of the string found
r = b[sLen:] r = d[sLen:]
b = b[:sLen] d = d[:sLen]
for _, ch := range b { for _, ch := range d {
ch -= zero ch -= zero
n.N = n.N*10 + uint64(ch) n.N = n.N*10 + uint64(ch)
} }

View File

@@ -1,4 +1,4 @@
package timestamp package decimal
import ( import (
"math" "math"

9
pkg/decimal/log.go Normal file
View File

@@ -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
)

View File

@@ -2,18 +2,72 @@ package event
import ( import (
"protocol.realy.lol/pkg/content" "protocol.realy.lol/pkg/content"
"protocol.realy.lol/pkg/decimal"
"protocol.realy.lol/pkg/event/types" "protocol.realy.lol/pkg/event/types"
"protocol.realy.lol/pkg/pubkey" "protocol.realy.lol/pkg/pubkey"
"protocol.realy.lol/pkg/signature" "protocol.realy.lol/pkg/signature"
"protocol.realy.lol/pkg/tags" "protocol.realy.lol/pkg/tags"
"protocol.realy.lol/pkg/timestamp"
) )
type Event struct { type E struct {
Type *types.T Type *types.T
Pubkey *pubkey.P Pubkey *pubkey.P
Timestamp *timestamp.T Timestamp *decimal.T
Tags *tags.T Tags *tags.T
Content *content.C Content *content.C
Signature *signature.S 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
}

9
pkg/event/event_test.go Normal file
View File

@@ -0,0 +1,9 @@
package event
import (
"testing"
)
func TestE_Marshal_Unmarshal(t *testing.T) {
}

View File

@@ -8,38 +8,38 @@ import (
// A T is a type descriptor, that is terminated by a newline. // A T is a type descriptor, that is terminated by a newline.
type T struct{ t []byte } 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) } 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 // Marshal append the T to a slice and appends a terminal newline, and returns
// the result. // 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 { if t == nil {
return return
} }
result = append(append(dst, t.t...), '\n') r = append(append(d, t.t...), '\n')
return return
} }
// Unmarshal expects an identifier followed by a newline. If the buffer ends // Unmarshal expects an identifier followed by a newline. If the buffer ends
// without a newline an EOF is returned. // without a newline an EOF is returned.
func (t *T) Unmarshal(data []byte) (rem []byte, err error) { func (t *T) Unmarshal(d []byte) (r []byte, err error) {
rem = data r = d
if t == nil { if t == nil {
err = errorf.E("can't unmarshal into nil types.T") err = errorf.E("can't unmarshal into nil types.T")
return return
} }
if len(rem) < 2 { if len(r) < 2 {
err = errorf.E("can't unmarshal nothing") err = errorf.E("can't unmarshal nothing")
return return
} }
for i := range rem { for i := range r {
if rem[i] == '\n' { if r[i] == '\n' {
// write read data up to the newline and return the remainder after // write read data up to the newline and return the remainder after
// the newline. // the newline.
t.t = rem[:i] t.t = r[:i]
rem = rem[i+1:] r = r[i+1:]
return return
} }
} }

View File

@@ -21,8 +21,8 @@ func New(id []byte) (p *P, err error) {
return return
} }
func (p *P) Marshal(dst []byte) (result []byte, err error) { func (p *P) Marshal(d []byte) (r []byte, err error) {
result = dst r = d
if p == nil || p.b == nil || len(p.b) == 0 { if p == nil || p.b == nil || len(p.b) == 0 {
err = errorf.E("nil/zero length pubkey") err = errorf.E("nil/zero length pubkey")
return return
@@ -32,7 +32,7 @@ func (p *P) Marshal(dst []byte) (result []byte, err error) {
len(p.b), ed25519.PublicKeySize, p.b) len(p.b), ed25519.PublicKeySize, p.b)
return return
} }
buf := bytes.NewBuffer(result) buf := bytes.NewBuffer(r)
w := base64.NewEncoder(base64.RawURLEncoding, buf) w := base64.NewEncoder(base64.RawURLEncoding, buf)
if _, err = w.Write(p.b); chk.E(err) { if _, err = w.Write(p.b); chk.E(err) {
return return
@@ -40,32 +40,32 @@ func (p *P) Marshal(dst []byte) (result []byte, err error) {
if err = w.Close(); chk.E(err) { if err = w.Close(); chk.E(err) {
return return
} }
result = append(buf.Bytes(), '\n') r = append(buf.Bytes(), '\n')
return return
} }
func (p *P) Unmarshal(data []byte) (rem []byte, err error) { func (p *P) Unmarshal(data []byte) (r []byte, err error) {
rem = data r = data
if p == nil { if p == nil {
err = errorf.E("can't unmarshal into nil types.T") err = errorf.E("can't unmarshal into nil types.T")
return return
} }
if len(rem) < 2 { if len(r) < 2 {
err = errorf.E("can't unmarshal nothing") err = errorf.E("can't unmarshal nothing")
return return
} }
for i := range rem { for i := range r {
if rem[i] == '\n' { if r[i] == '\n' {
if i != Len { if i != Len {
err = errorf.E("invalid encoded pubkey length %d; require %d '%0x'", err = errorf.E("invalid encoded pubkey length %d; require %d '%0x'",
i, Len, rem[:i]) i, Len, r[:i])
return return
} }
p.b = make([]byte, ed25519.PublicKeySize) 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 return
} }
rem = rem[i+1:] r = r[i+1:]
return return
} }
} }

View File

@@ -21,8 +21,8 @@ func New(pk []byte) (p *P, err error) {
return return
} }
func (p *P) Marshal(dst []byte) (result []byte, err error) { func (p *P) Marshal(d []byte) (r []byte, err error) {
result = dst r = d
if p == nil || p.PublicKey == nil || len(p.PublicKey) == 0 { if p == nil || p.PublicKey == nil || len(p.PublicKey) == 0 {
err = errorf.E("nil/zero length pubkey") err = errorf.E("nil/zero length pubkey")
return return
@@ -32,7 +32,7 @@ func (p *P) Marshal(dst []byte) (result []byte, err error) {
len(p.PublicKey), ed25519.PublicKeySize, p.PublicKey) len(p.PublicKey), ed25519.PublicKeySize, p.PublicKey)
return return
} }
buf := bytes.NewBuffer(result) buf := bytes.NewBuffer(r)
w := base64.NewEncoder(base64.RawURLEncoding, buf) w := base64.NewEncoder(base64.RawURLEncoding, buf)
if _, err = w.Write(p.PublicKey); chk.E(err) { if _, err = w.Write(p.PublicKey); chk.E(err) {
return return
@@ -40,32 +40,32 @@ func (p *P) Marshal(dst []byte) (result []byte, err error) {
if err = w.Close(); chk.E(err) { if err = w.Close(); chk.E(err) {
return return
} }
result = append(buf.Bytes(), '\n') r = append(buf.Bytes(), '\n')
return return
} }
func (p *P) Unmarshal(data []byte) (rem []byte, err error) { func (p *P) Unmarshal(d []byte) (r []byte, err error) {
rem = data r = d
if p == nil { if p == nil {
err = errorf.E("can't unmarshal into nil types.T") err = errorf.E("can't unmarshal into nil types.T")
return return
} }
if len(rem) < 2 { if len(r) < 2 {
err = errorf.E("can't unmarshal nothing") err = errorf.E("can't unmarshal nothing")
return return
} }
for i := range rem { for i := range r {
if rem[i] == '\n' { if r[i] == '\n' {
if i != Len { if i != Len {
err = errorf.E("invalid encoded pubkey length %d; require %d '%0x'", err = errorf.E("invalid encoded pubkey length %d; require %d '%0x'",
i, Len, rem[:i]) i, Len, r[:i])
return return
} }
p.PublicKey = make([]byte, ed25519.PublicKeySize) 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 return
} }
rem = rem[i+1:] r = r[i+1:]
return return
} }
} }

View File

@@ -21,8 +21,16 @@ func New(sig []byte) (p *S, err error) {
return return
} }
func (p *S) Marshal(dst []byte) (result []byte, err error) { func Sign(msg []byte, sec ed25519.PrivateKey) (sig []byte, err error) {
result = dst 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 { if p == nil || p.Signature == nil || len(p.Signature) == 0 {
err = errorf.E("nil/zero length signature") err = errorf.E("nil/zero length signature")
return return
@@ -32,7 +40,7 @@ func (p *S) Marshal(dst []byte) (result []byte, err error) {
len(p.Signature), ed25519.SignatureSize, p.Signature) len(p.Signature), ed25519.SignatureSize, p.Signature)
return return
} }
buf := bytes.NewBuffer(result) buf := bytes.NewBuffer(r)
w := base64.NewEncoder(base64.RawURLEncoding, buf) w := base64.NewEncoder(base64.RawURLEncoding, buf)
if _, err = w.Write(p.Signature); chk.E(err) { if _, err = w.Write(p.Signature); chk.E(err) {
return return
@@ -40,32 +48,32 @@ func (p *S) Marshal(dst []byte) (result []byte, err error) {
if err = w.Close(); chk.E(err) { if err = w.Close(); chk.E(err) {
return return
} }
result = append(buf.Bytes(), '\n') r = append(buf.Bytes(), '\n')
return return
} }
func (p *S) Unmarshal(data []byte) (rem []byte, err error) { func (p *S) Unmarshal(d []byte) (r []byte, err error) {
rem = data r = d
if p == nil { if p == nil {
err = errorf.E("can't unmarshal into nil types.T") err = errorf.E("can't unmarshal into nil types.T")
return return
} }
if len(rem) < 2 { if len(r) < 2 {
err = errorf.E("can't unmarshal nothing") err = errorf.E("can't unmarshal nothing")
return return
} }
for i := range rem { for i := range r {
if rem[i] == '\n' { if r[i] == '\n' {
if i != Len { if i != Len {
err = errorf.E("invalid encoded signature length %d; require %d '%0x'", err = errorf.E("invalid encoded signature length %d; require %d '%0x'",
i, Len, rem[:i]) i, Len, r[:i])
return return
} }
p.Signature = make([]byte, ed25519.SignatureSize) 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 return
} }
rem = rem[i+1:] r = r[i+1:]
return return
} }
} }

View File

@@ -77,32 +77,32 @@ func ValidateField[V ~[]byte | ~string](f V, i int) (k []byte, err error) {
return return
} }
func (t *T) Marshal(dst []byte) (result []byte, err error) { func (t *T) Marshal(d []byte) (r []byte, err error) {
result = dst r = d
if len(t.fields) == 0 { if len(t.fields) == 0 {
return return
} }
for i, field := range t.fields { for i, field := range t.fields {
result = append(result, field...) r = append(r, field...)
if i == 0 { if i == 0 {
result = append(result, ':') r = append(r, ':')
} else if i == len(t.fields)-1 { } else if i == len(t.fields)-1 {
result = append(result, '\n') r = append(r, '\n')
} else { } else {
result = append(result, ';') r = append(r, ';')
} }
} }
return 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 i int
var v byte var v byte
var dat []byte var dat []byte
// first find the end // first find the end
for i, v = range data { for i, v = range d {
if v == '\n' { if v == '\n' {
dat, rem = data[:i], data[i+1:] dat, r = d[:i], d[i+1:]
break break
} }
} }

View File

@@ -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. 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:./relays/readme.adoc[reference relays]
* link:./repos/readme.adoc[reference repos]
* link:./clients/readme.adoc[reference clients] * link:./clients/readme.adoc[reference clients]
* link:./pkg/readme.adoc[_GO⌯_ libraries] * 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. 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 store\n
<event> <event>
replace:<event id>\n update:<event id>\n
<event> <event>
relay:\n relay:\n
@@ -267,12 +270,14 @@ Events that are returned have the `<subscription Id>:<Event Id>\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. 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. 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: Thus, a primary query type is the simple request for a list of events by their ID hash:
==== Request ===== Request
.events request .events request
[options="header"] [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: The results are returned as a series as follows, for each item returned:
==== Response ===== Response
.events response .events response
[options="header"] [options="header"]
@@ -299,11 +304,11 @@ The results are returned as a series as follows, for each item returned:
|`<event>\n`| the full event text as described previously |`<event>\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`: A filter has one or more of the fields listed below, and headed with `filter`:
==== Request ===== Request
.filter request .filter request
[options="header"] [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: 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"] [options="header"]
|==== |====
| Message | Description | 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. |`...` | ...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:<subscription id>\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:<one>;<two>;...\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:<one>;<two>;...\n` | list of pubkeys to only return results from
|`tags:\n` | these end with a second newline
|`<key>:<value>[;...]\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:<subscription id>\n` |
|====
To close a subscription the client sends an `unsubscribe`:
.unsubscribe request
[options="header"]
|====
| Message | Description
|`unsubscribe:<subscription id>\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) 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. The `subscribe` query streams back results containing just the event ID hash, in the following message:
For this reason an empty `subscribe` is implicitly "from now".
.subscription response
[options="header"]
|====
| Message | Description
|`subscription:<subscription id>:<event id>\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. 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. 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. 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.

View File

@@ -1,3 +1,3 @@
= relays = relays
relay implementations for various subprotocols relays specialise in subscription and relaying, and do not store events

View File

@@ -1,3 +1,3 @@
= relays = repos
relay implementations for various subprotocols repos are relays that store data