update drafts of several subdirectories and main readme.adoc
This commit is contained in:
3
directories/readme.adoc
Normal file
3
directories/readme.adoc
Normal file
@@ -0,0 +1,3 @@
|
||||
= directories
|
||||
|
||||
repos are relays that store data about users, including access privilege lists for other relays
|
||||
4
go.mod
4
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
|
||||
)
|
||||
|
||||
4
go.sum
4
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=
|
||||
|
||||
47
pkg/auth/auth.go
Normal file
47
pkg/auth/auth.go
Normal 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
1
pkg/auth/auth_test.go
Normal file
@@ -0,0 +1 @@
|
||||
package auth
|
||||
@@ -1,4 +1,4 @@
|
||||
package timestamp
|
||||
package auth
|
||||
|
||||
import (
|
||||
"protocol.realy.lol/pkg/lol"
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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:<length>\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:<length>:\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:<length>\\n, got '%s' %x", c.Content[len(c.Content)-1])
|
||||
return
|
||||
}
|
||||
r = r[1:]
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package timestamp
|
||||
package decimal
|
||||
|
||||
import (
|
||||
"math"
|
||||
9
pkg/decimal/log.go
Normal file
9
pkg/decimal/log.go
Normal 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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
9
pkg/event/event_test.go
Normal file
9
pkg/event/event_test.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestE_Marshal_Unmarshal(t *testing.T) {
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
24
pkg/id/id.go
24
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
75
readme.adoc
75
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
|
||||
<event>
|
||||
|
||||
replace:<event id>\n
|
||||
update:<event id>\n
|
||||
<event>
|
||||
|
||||
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.
|
||||
|
||||
=== `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:
|
||||
|`<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`:
|
||||
|
||||
==== 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:<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)
|
||||
|
||||
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:<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.
|
||||
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.
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
= relays
|
||||
|
||||
relay implementations for various subprotocols
|
||||
relays specialise in subscription and relaying, and do not store events
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
= relays
|
||||
= repos
|
||||
|
||||
relay implementations for various subprotocols
|
||||
repos are relays that store data
|
||||
|
||||
Reference in New Issue
Block a user