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/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
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=
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
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 (
"protocol.realy.lol/pkg/lol"

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package timestamp
package decimal
import (
"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 (
"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
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.
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
}
}

View File

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

View File

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

View File

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

View File

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

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.
* 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.

View File

@@ -1,3 +1,3 @@
= 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