diff --git a/.golangci.yml b/.golangci.yml index 82dd973..922d6d5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -45,7 +45,7 @@ linters: - makezero - mirror - misspell - - musttag + # - musttag - nakedret - nilerr - nilnil diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..db71d96 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,13 @@ +package main + +import "github.com/dezh-tech/immortal/relay" + +// TODO::: create a full functioning CLI to manage rely. + +func main() { + s := relay.NewRelay() + err := s.Start() + if err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod index 03e7a0a..58ad8e5 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/mailru/easyjson v0.7.7 github.com/stretchr/testify v1.9.0 github.com/tidwall/gjson v1.17.3 + golang.org/x/net v0.29.0 ) require ( diff --git a/go.sum b/go.sum index fd7869a..0aa1fcd 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/relay/relay.go b/relay/relay.go new file mode 100644 index 0000000..eaf7673 --- /dev/null +++ b/relay/relay.go @@ -0,0 +1,192 @@ +package relay + +import ( + "errors" + "fmt" + "io" + "log" + "net/http" + "sync" + + "github.com/dezh-tech/immortal/types/filter" + "github.com/dezh-tech/immortal/types/message" + "golang.org/x/net/websocket" +) + +// TODO::: replace with https://github.com/coder/websocket. +// TODO::: replace `log` with main logger. + +// Relay represents a nostr relay which keeps track of client connections and handle them. +type Relay struct { + conns map[*websocket.Conn]map[string]filter.Filters + connsLock sync.RWMutex +} + +func NewRelay() *Relay { + return &Relay{ + conns: make(map[*websocket.Conn]map[string]filter.Filters), + connsLock: sync.RWMutex{}, + } +} + +// Start strats a new relay instance. +func (r *Relay) Start() error { + http.Handle("/ws", websocket.Handler(r.handleWS)) + err := http.ListenAndServe(":3000", nil) //nolint + + return err +} + +// handleWS is WebSocket handler. +func (r *Relay) handleWS(ws *websocket.Conn) { + log.Printf("new connection: %s\n", ws.RemoteAddr()) + + r.connsLock.Lock() + r.conns[ws] = make(map[string]filter.Filters) + r.connsLock.Unlock() + + r.readLoop(ws) +} + +// readLoop reads incoming messages from a client and answer to them. +func (r *Relay) readLoop(ws *websocket.Conn) { + buf := make([]byte, 1024) + for { + n, err := ws.Read(buf) + if err != nil { + if errors.Is(err, io.EOF) { + break + } + + log.Printf("error in connection handling: %s\n", err) + + continue + } + + msg := message.ParseMessage(buf[:n]) + if msg == nil { + _, _ = ws.Write(message.MakeNotice("error: can't parse message.")) + + continue + } + + log.Printf("received envelope: %s\n", msg.String()) + + switch msg.Type() { + case "REQ": + go r.handleReq(ws, msg) + + case "EVENT": + go r.handleEvent(ws, msg) + + case "CLOSE": + r.handleClose(ws, msg) + } + } +} + +// handleReq handles new incoming REQ messages from client. +func (r *Relay) handleReq(ws *websocket.Conn, m message.Message) { + // TODO::: loadfrom database and sent in first query based on limit. + // TODO::: return EOSE. + // TODO::: return EVENT messages. + + msg, ok := m.(*message.Req) + if !ok { + _, _ = ws.Write(message.MakeNotice("error: can't parse REQ message.")) + + return + } + + r.connsLock.Lock() + defer r.connsLock.Unlock() + + subs, ok := r.conns[ws] + if !ok { + _, _ = ws.Write(message.MakeNotice(fmt.Sprintf("error: can't find connection %s.", + ws.RemoteAddr()))) + + return + } + + subs[msg.SubscriptionID] = msg.Filters +} + +// handleEvent handles new incoming EVENT messages from client. +func (r *Relay) handleEvent(ws *websocket.Conn, m message.Message) { + msg, ok := m.(*message.Event) + if !ok { + okm := message.MakeOK(false, + "", + "error: can't parse EVENT message.", + ) + + _, _ = ws.Write(okm) + + return + } + + if !msg.Event.IsValid() { + okm := message.MakeOK(false, + msg.SubscriptionID, + "invalid: id or sig is not correct.", + ) + + _, _ = ws.Write(okm) + + return + } + + _, _ = ws.Write(message.MakeOK(true, msg.SubscriptionID, "")) + + for conn, subs := range r.conns { + for id, filters := range subs { + if !filters.Match(msg.Event) { + return + } + _, _ = conn.Write(message.MakeEvent(id, msg.Event)) + } + } +} + +// handleClose handles new incoming CLOSE messages from client. +func (r *Relay) handleClose(ws *websocket.Conn, m message.Message) { + msg, ok := m.(*message.Close) + if !ok { + _, _ = ws.Write(message.MakeNotice("error: can't parse CLOSE message.")) + + return + } + + r.connsLock.Lock() + defer r.connsLock.Unlock() + + conn, ok := r.conns[ws] + if !ok { + _, _ = ws.Write(message.MakeNotice(fmt.Sprintf("error: can't find connection %s.", + ws.RemoteAddr()))) + + return + } + + delete(conn, msg.String()) + _, _ = ws.Write(message.MakeClosed(msg.String(), "ok: closed successfully.")) +} + +// Stop shutdowns the relay gracefully. +func (r *Relay) Stop() error { + r.connsLock.Lock() + defer r.connsLock.Unlock() + + for wsConn, subs := range r.conns { + // close all subscriptions. + for id := range subs { + _, _ = wsConn.Write(message.MakeClosed(id, "error: shutdowning the relay.")) + } + + // close connection. + _ = wsConn.Close() + } + + return nil +} diff --git a/types/envelope/envelope.go b/types/envelope/envelope.go deleted file mode 100644 index ed8226c..0000000 --- a/types/envelope/envelope.go +++ /dev/null @@ -1,460 +0,0 @@ -package envelope - -import ( - "bytes" - "encoding/json" - "fmt" - "strconv" - - "github.com/dezh-tech/immortal/types/event" - "github.com/dezh-tech/immortal/types/filter" - "github.com/mailru/easyjson" - jwriter "github.com/mailru/easyjson/jwriter" - "github.com/tidwall/gjson" // TODO::: remove/replace me! -) - -func ParseMessage(message []byte) Envelope { - firstComma := bytes.Index(message, []byte{','}) - if firstComma == -1 { - return nil - } - label := message[0:firstComma] - - var v Envelope - switch { - case bytes.Contains(label, []byte("EVENT")): - v = &EventEnvelope{} - case bytes.Contains(label, []byte("REQ")): - v = &ReqEnvelope{} - case bytes.Contains(label, []byte("COUNT")): - v = &CountEnvelope{} - case bytes.Contains(label, []byte("NOTICE")): - x := NoticeEnvelope("") - v = &x - case bytes.Contains(label, []byte("EOSE")): - x := EOSEEnvelope("") - v = &x - case bytes.Contains(label, []byte("OK")): - v = &OKEnvelope{} - case bytes.Contains(label, []byte("AUTH")): - v = &AuthEnvelope{} - case bytes.Contains(label, []byte("CLOSED")): - v = &ClosedEnvelope{} - case bytes.Contains(label, []byte("CLOSE")): - x := CloseEnvelope("") - v = &x - default: - return nil - } - - if err := v.UnmarshalJSON(message); err != nil { - return nil - } - - return v -} - -type Envelope interface { - Label() string - UnmarshalJSON([]byte) error - MarshalJSON() ([]byte, error) - String() string -} - -var ( - _ Envelope = (*EventEnvelope)(nil) - _ Envelope = (*ReqEnvelope)(nil) - _ Envelope = (*CountEnvelope)(nil) - _ Envelope = (*NoticeEnvelope)(nil) - _ Envelope = (*EOSEEnvelope)(nil) - _ Envelope = (*CloseEnvelope)(nil) - _ Envelope = (*OKEnvelope)(nil) - _ Envelope = (*AuthEnvelope)(nil) -) - -type EventEnvelope struct { - SubscriptionID string - Event *event.Event -} - -func (EventEnvelope) Label() string { return "EVENT" } - -func (ee EventEnvelope) String() string { - v, err := json.Marshal(ee) - if err != nil { - return "" - } - - return string(v) -} - -func (ee *EventEnvelope) UnmarshalJSON(data []byte) error { - r := gjson.ParseBytes(data) - arr := r.Array() - switch len(arr) { - case 2: - - return easyjson.Unmarshal([]byte(arr[1].Raw), ee.Event) - case 3: - ee.SubscriptionID = arr[1].Str - - return easyjson.Unmarshal([]byte(arr[2].Raw), ee.Event) - default: - - return fmt.Errorf("failed to decode EVENT envelope") - } -} - -func (ee EventEnvelope) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - w.RawString(`["EVENT",`) - - if ee.SubscriptionID != "" { - w.RawString(`"` + ee.SubscriptionID + `",`) - } - - ee.Event.MarshalEasyJSON(&w) - w.RawString(`]`) - - return w.BuildBytes() -} - -type ReqEnvelope struct { - SubscriptionID string - filter.Filters -} - -func (ReqEnvelope) Label() string { return "REQ" } - -func (re *ReqEnvelope) UnmarshalJSON(data []byte) error { - r := gjson.ParseBytes(data) - arr := r.Array() - if len(arr) < 3 { - return fmt.Errorf("failed to decode REQ envelope: missing filters") - } - re.SubscriptionID = arr[1].Str - re.Filters = make(filter.Filters, len(arr)-2) - f := 0 - for i := 2; i < len(arr); i++ { - if err := easyjson.Unmarshal([]byte(arr[i].Raw), &re.Filters[f]); err != nil { - return fmt.Errorf("%w -- on filter %d", err, f) - } - f++ - } - - return nil -} - -func (re ReqEnvelope) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - w.RawString(`["REQ",`) - w.RawString(`"` + re.SubscriptionID + `"`) - for _, filter := range re.Filters { - w.RawString(`,`) - filter.MarshalEasyJSON(&w) - } - w.RawString(`]`) - - return w.BuildBytes() -} - -type CountEnvelope struct { - SubscriptionID string - Filters []*filter.Filter - Count *int64 -} - -func (CountEnvelope) Label() string { return "COUNT" } -func (ce CountEnvelope) String() string { - v, err := json.Marshal(ce) - if err != nil { - return "" - } - - return string(v) -} - -func (ce *CountEnvelope) UnmarshalJSON(data []byte) error { - r := gjson.ParseBytes(data) - arr := r.Array() - if len(arr) < 3 { - return fmt.Errorf("failed to decode COUNT envelope: missing filters") - } - ce.SubscriptionID = arr[1].Str - - if len(arr) < 3 { - return fmt.Errorf("COUNT array must have at least 3 items") - } - - var countResult struct { - Count *int64 `json:"count"` - } - if err := json.Unmarshal([]byte(arr[2].Raw), &countResult); err == nil && countResult.Count != nil { - ce.Count = countResult.Count - - return nil - } - - ce.Filters = make([]*filter.Filter, len(arr)-2) - f := 0 - for i := 2; i < len(arr); i++ { - item := []byte(arr[i].Raw) - - if err := easyjson.Unmarshal(item, ce.Filters[f]); err != nil { - return fmt.Errorf("%w -- on filter %d", err, f) - } - - f++ - } - - return nil -} - -func (ce CountEnvelope) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - w.RawString(`["COUNT",`) - w.RawString(`"` + ce.SubscriptionID + `"`) - if ce.Count != nil { - w.RawString(`,{"count":`) - w.RawString(strconv.FormatInt(*ce.Count, 10)) - w.RawString(`}`) - } else { - for _, filter := range ce.Filters { - w.RawString(`,`) - filter.MarshalEasyJSON(&w) - } - } - w.RawString(`]`) - - return w.BuildBytes() -} - -type NoticeEnvelope string - -func (NoticeEnvelope) Label() string { return "NOTICE" } -func (ne NoticeEnvelope) String() string { - v, err := json.Marshal(ne) - if err != nil { - return "" - } - - return string(v) -} - -func (ne *NoticeEnvelope) UnmarshalJSON(data []byte) error { - r := gjson.ParseBytes(data) - arr := r.Array() - if len(arr) < 2 { - return fmt.Errorf("failed to decode NOTICE envelope") - } - *ne = NoticeEnvelope(arr[1].Str) - - return nil -} - -func (ne NoticeEnvelope) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - w.RawString(`["NOTICE",`) - w.Raw(json.Marshal(string(ne))) - w.RawString(`]`) - - return w.BuildBytes() -} - -type EOSEEnvelope string - -func (EOSEEnvelope) Label() string { return "EOSE" } -func (ee EOSEEnvelope) String() string { - v, err := json.Marshal(ee) - if err != nil { - return "" - } - - return string(v) -} - -func (ee *EOSEEnvelope) UnmarshalJSON(data []byte) error { - r := gjson.ParseBytes(data) - arr := r.Array() - if len(arr) < 2 { - return fmt.Errorf("failed to decode EOSE envelope") - } - *ee = EOSEEnvelope(arr[1].Str) - - return nil -} - -func (ee EOSEEnvelope) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - w.RawString(`["EOSE",`) - w.Raw(json.Marshal(string(ee))) - w.RawString(`]`) - - return w.BuildBytes() -} - -type CloseEnvelope string - -func (CloseEnvelope) Label() string { return "CLOSE" } -func (ce CloseEnvelope) String() string { - v, err := json.Marshal(ce) - if err != nil { - return "" - } - - return string(v) -} - -func (ce *CloseEnvelope) UnmarshalJSON(data []byte) error { - r := gjson.ParseBytes(data) - arr := r.Array() - switch len(arr) { - case 2: - *ce = CloseEnvelope(arr[1].Str) - - return nil - default: - - return fmt.Errorf("failed to decode CLOSE envelope") - } -} - -func (ce CloseEnvelope) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - w.RawString(`["CLOSE",`) - w.Raw(json.Marshal(string(ce))) - w.RawString(`]`) - - return w.BuildBytes() -} - -type ClosedEnvelope struct { - SubscriptionID string - Reason string -} - -func (ClosedEnvelope) Label() string { return "CLOSED" } -func (ce ClosedEnvelope) String() string { - v, err := json.Marshal(ce) - if err != nil { - return "" - } - - return string(v) -} - -func (ce *ClosedEnvelope) UnmarshalJSON(data []byte) error { - r := gjson.ParseBytes(data) - arr := r.Array() - switch len(arr) { - case 3: - *ce = ClosedEnvelope{arr[1].Str, arr[2].Str} - - return nil - default: - - return fmt.Errorf("failed to decode CLOSED envelope") - } -} - -func (ce ClosedEnvelope) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - w.RawString(`["CLOSED",`) - w.Raw(json.Marshal(ce.SubscriptionID)) - w.RawString(`,`) - w.Raw(json.Marshal(ce.Reason)) - w.RawString(`]`) - - return w.BuildBytes() -} - -type OKEnvelope struct { - EventID string - OK bool - Reason string -} - -func (OKEnvelope) Label() string { return "OK" } -func (oe OKEnvelope) String() string { - v, err := json.Marshal(oe) - if err != nil { - return "" - } - - return string(v) -} - -func (oe *OKEnvelope) UnmarshalJSON(data []byte) error { - r := gjson.ParseBytes(data) - arr := r.Array() - if len(arr) < 4 { - return fmt.Errorf("failed to decode OK envelope: missing fields") - } - oe.EventID = arr[1].Str - oe.OK = arr[2].Raw == "true" - oe.Reason = arr[3].Str - - return nil -} - -func (oe OKEnvelope) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - w.RawString(`["OK",`) - w.RawString(`"` + oe.EventID + `",`) - ok := "false" - if oe.OK { - ok = "true" - } - w.RawString(ok) - w.RawString(`,`) - w.Raw(json.Marshal(oe.Reason)) - w.RawString(`]`) - - return w.BuildBytes() -} - -type AuthEnvelope struct { - Challenge *string - Event *event.Event -} - -func (AuthEnvelope) Label() string { return "AUTH" } - -func (ae AuthEnvelope) String() string { - v, err := json.Marshal(ae) - if err != nil { - return "" - } - - return string(v) -} - -func (ae *AuthEnvelope) UnmarshalJSON(data []byte) error { - r := gjson.ParseBytes(data) - arr := r.Array() - if len(arr) < 2 { - return fmt.Errorf("failed to decode Auth envelope: missing fields") - } - - if arr[1].IsObject() { - return easyjson.Unmarshal([]byte(arr[1].Raw), ae.Event) - } - - ae.Challenge = &arr[1].Str - - return nil -} - -func (ae AuthEnvelope) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - w.RawString(`["AUTH",`) - if ae.Challenge != nil { - w.Raw(json.Marshal(*ae.Challenge)) - } else { - ae.Event.MarshalEasyJSON(&w) - } - - w.RawString(`]`) - - return w.BuildBytes() -} diff --git a/types/errors.go b/types/errors.go new file mode 100644 index 0000000..011817e --- /dev/null +++ b/types/errors.go @@ -0,0 +1,21 @@ +package types + +import "fmt" + +// EncodeError represents an encoding error. +type EncodeError struct { + Reason string +} + +func (e EncodeError) Error() string { + return fmt.Sprintf("encoding error: %s", e.Reason) +} + +// DecodeError represents an decoding error. +type DecodeError struct { + Reason string +} + +func (e DecodeError) Error() string { + return fmt.Sprintf("decoding error: %s", e.Reason) +} diff --git a/types/event/event.go b/types/event/event.go index 2d64124..47945d7 100644 --- a/types/event/event.go +++ b/types/event/event.go @@ -26,7 +26,9 @@ func Decode(b []byte) (*Event, error) { e := new(Event) if err := easyjson.Unmarshal(b, e); err != nil { - return nil, err + return nil, types.DecodeError{ + Reason: err.Error(), + } } return e, nil @@ -36,7 +38,9 @@ func Decode(b []byte) (*Event, error) { func (e *Event) Encode() ([]byte, error) { b, err := easyjson.Marshal(e) if err != nil { - return nil, err + return nil, types.EncodeError{ + Reason: err.Error(), + } } return b, nil @@ -47,7 +51,7 @@ func (e *Event) Serialize() []byte { // so the order is kept. See NIP-01 dst := make([]byte, 0) - // the header portion is easy to serialize + // the header portion is easy to serialize. // [0,"pubkey",created_at,kind,[ dst = append(dst, []byte( fmt.Sprintf( //nolint @@ -57,7 +61,7 @@ func (e *Event) Serialize() []byte { e.Kind, ))...) - // tags + // tags. dst = types.MarshalTo(e.Tags, dst) dst = append(dst, ',') @@ -70,6 +74,11 @@ func (e *Event) Serialize() []byte { // IsValid function validats an event Signature and ID. func (e *Event) IsValid() bool { + id := sha256.Sum256(e.Serialize()) + if hex.EncodeToString(id[:]) != e.ID { + return false + } + pk, err := hex.DecodeString(e.PublicKey) if err != nil { return false @@ -90,8 +99,16 @@ func (e *Event) IsValid() bool { return false } - hash := sha256.Sum256(e.Serialize()) - // TODO::: replace with libsecp256k1 (C++ version). - return sig.Verify(hash[:], pubkey) + return sig.Verify(id[:], pubkey) +} + +// String returns and encoded string representation of event e. +func (e *Event) String() string { + ee, err := e.Encode() + if err != nil { + return "" + } + + return string(ee) } diff --git a/types/event/event_easyjson.go b/types/event/event_easyjson.go index 861f722..3db42bf 100644 --- a/types/event/event_easyjson.go +++ b/types/event/event_easyjson.go @@ -183,10 +183,10 @@ func (e Event) MarshalEasyJSON(w *jwriter.Writer) { //nolint // UnmarshalJSON supports json.Unmarshaler interface. func (e *Event) UnmarshalJSON(data []byte) error { //nolint - r := jlexer.Lexer{Data: data} - easyjsonF642ad3eDecodeGithubComDezhTechImmortalTypesEvent(&r, e) + l := jlexer.Lexer{Data: data} + easyjsonF642ad3eDecodeGithubComDezhTechImmortalTypesEvent(&l, e) - return r.Error() + return l.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface. diff --git a/types/filter/filter.go b/types/filter/filter.go index ac32092..eed8278 100644 --- a/types/filter/filter.go +++ b/types/filter/filter.go @@ -81,7 +81,9 @@ func Decode(b []byte) (*Filter, error) { e := new(Filter) if err := easyjson.Unmarshal(b, e); err != nil { - return nil, err + return nil, types.DecodeError{ + Reason: err.Error(), + } } return e, nil @@ -91,7 +93,9 @@ func Decode(b []byte) (*Filter, error) { func (f *Filter) Encode() ([]byte, error) { ee, err := easyjson.Marshal(f) if err != nil { - return nil, err + return nil, types.EncodeError{ + Reason: err.Error(), + } } return ee, nil diff --git a/types/message/message.go b/types/message/message.go new file mode 100644 index 0000000..7951968 --- /dev/null +++ b/types/message/message.go @@ -0,0 +1,511 @@ +package message + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + + "github.com/dezh-tech/immortal/types" + "github.com/dezh-tech/immortal/types/event" + "github.com/dezh-tech/immortal/types/filter" + "github.com/mailru/easyjson" + jwriter "github.com/mailru/easyjson/jwriter" + "github.com/tidwall/gjson" // TODO::: remove/replace me! +) + +// Message reperesents an NIP-01 message which can be sent to or received by client. +type Message interface { + Type() string + DecodeFromJSON([]byte) error + EncodeToJSON() ([]byte, error) + String() string +} + +// ParseMessage parses the given message from client to a message interface. +func ParseMessage(message []byte) Message { + firstComma := bytes.Index(message, []byte{','}) + if firstComma == -1 { + return nil + } + label := message[0:firstComma] + + var e Message + switch { + case bytes.Contains(label, []byte("EVENT")): + e = &Event{ + Event: new(event.Event), + } + case bytes.Contains(label, []byte("REQ")): + e = &Req{} + case bytes.Contains(label, []byte("COUNT")): + e = &Count{} + case bytes.Contains(label, []byte("AUTH")): + e = &Auth{} + case bytes.Contains(label, []byte("CLOSE")): + x := Close("") + e = &x + default: + return nil + } + + if err := e.DecodeFromJSON(message); err != nil { + return nil + } + + return e +} + +// Event reperesents a NIP-01 EVENT message. +type Event struct { + SubscriptionID string + Event *event.Event +} + +// MakeEvent constructs an EVENT message to be sent to client. +func MakeEvent(id string, e *event.Event) []byte { + em := Event{ + SubscriptionID: id, + Event: e, + } + + res, err := em.EncodeToJSON() + if err != nil { + return []byte{} // TODO::: should we return anything else here? + } + + return res +} + +func (Event) Type() string { return "EVENT" } + +func (em Event) String() string { + v, err := json.Marshal(em) + if err != nil { + return "" + } + + return string(v) +} + +func (em *Event) DecodeFromJSON(data []byte) error { + r := gjson.ParseBytes(data) + arr := r.Array() + switch { + case len(arr) >= 2: + err := easyjson.Unmarshal([]byte(arr[1].Raw), em.Event) + if err != nil { + return types.DecodeError{ + Reason: fmt.Sprintf("EVENT message: %s", err.Error()), + } + } + + return nil + default: + + return types.DecodeError{ + Reason: "EVENT messag: no event found.", + } + } +} + +func (em Event) EncodeToJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["EVENT","` + em.SubscriptionID + `",`) + em.Event.MarshalEasyJSON(&w) + w.RawString(`]`) + + res, err := w.BuildBytes() + if err != nil { + return nil, types.EncodeError{ + Reason: fmt.Sprintf("EVENT message: %s", err.Error()), + } + } + + return res, nil +} + +// Req reperesents a NIP-01 REQ message. +type Req struct { + SubscriptionID string + filter.Filters +} + +func (Req) Type() string { return "REQ" } + +func (rm *Req) DecodeFromJSON(data []byte) error { + r := gjson.ParseBytes(data) + arr := r.Array() + if len(arr) < 3 { + return types.DecodeError{ + Reason: "REQ message: missing filters.", + } + } + rm.SubscriptionID = arr[1].Str + rm.Filters = make(filter.Filters, len(arr)-2) + f := 0 + for i := 2; i < len(arr); i++ { + if err := easyjson.Unmarshal([]byte(arr[i].Raw), &rm.Filters[f]); err != nil { + return types.DecodeError{ + Reason: fmt.Sprintf("REQ message: %s", err.Error()), + } + } + f++ + } + + return nil +} + +func (rm Req) EncodeToJSON() ([]byte, error) { + return nil, nil +} + +// Count reperesents a NIP-01 COUNT message. +type Count struct { + SubscriptionID string + Filters []*filter.Filter + Count int64 +} + +func (Count) Type() string { return "COUNT" } +func (cm Count) String() string { + v, err := json.Marshal(cm) + if err != nil { + return "" + } + + return string(v) +} + +func (cm *Count) DecodeFromJSON(data []byte) error { + r := gjson.ParseBytes(data) + arr := r.Array() + if len(arr) < 3 { + return types.DecodeError{ + Reason: "COUNT message: missing filters.", + } + } + cm.SubscriptionID = arr[1].Str + + if len(arr) < 3 { + return types.DecodeError{ + Reason: "COUNT message: array must have at least 3 items.", + } + } + + cm.Filters = make([]*filter.Filter, len(arr)-2) + f := 0 + for i := 2; i < len(arr); i++ { + item := []byte(arr[i].Raw) + + if err := easyjson.Unmarshal(item, cm.Filters[f]); err != nil { + return types.DecodeError{ + Reason: fmt.Sprintf("COUNT message: %s", err.Error()), + } + } + + f++ + } + + return nil +} + +func (cm Count) EncodeToJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["COUNT","` + cm.SubscriptionID + + `",{"count":` + strconv.FormatInt(cm.Count, 10) + `}]`) + + res, err := w.BuildBytes() + if err != nil { + return nil, types.EncodeError{ + Reason: fmt.Sprintf("COUNT message: %s", err.Error()), + } + } + + return res, nil +} + +// Notice reperesents a NIP-01 NOTICE message. +type Notice string + +func MakeNotice(msg string) []byte { + res, err := Notice(msg).EncodeToJSON() + if err != nil { + return []byte{} // TODO::: should we return anything else here? + } + + return res +} + +func (Notice) Type() string { return "NOTICE" } +func (nm Notice) String() string { + v, err := json.Marshal(nm) + if err != nil { + return "" + } + + return string(v) +} + +func (nm *Notice) DecodeFromJSON(_ []byte) error { + return nil +} + +func (nm Notice) EncodeToJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["NOTICE",`) + w.Raw(json.Marshal(string(nm))) + w.RawString(`]`) + + res, err := w.BuildBytes() + if err != nil { + return nil, types.EncodeError{ + Reason: fmt.Sprintf("NOTICE message: %s", err.Error()), + } + } + + return res, nil +} + +// EOSE reperesents a NIP-01 EOSE message. +type EOSE string + +func (EOSE) Type() string { return "EOSE" } +func (em EOSE) String() string { + v, err := json.Marshal(em) + if err != nil { + return "" + } + + return string(v) +} + +func (em *EOSE) DecodeFromJSON(_ []byte) error { + return nil +} + +func (em EOSE) EncodeToJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["EOSE",`) + w.Raw(json.Marshal(string(em))) + w.RawString(`]`) + + res, err := w.BuildBytes() + if err != nil { + return nil, types.EncodeError{ + Reason: fmt.Sprintf("EOSE message: %s", err.Error()), + } + } + + return res, nil +} + +// Close reperesents a NIP-01 CLOSE message. +type Close string + +func (Close) Type() string { return "CLOSE" } +func (cm Close) String() string { + return string(cm) +} + +func (cm *Close) DecodeFromJSON(data []byte) error { + r := gjson.ParseBytes(data) + arr := r.Array() + switch len(arr) { + case 2: + *cm = Close(arr[1].Str) + + return nil + default: + + return types.DecodeError{ + Reason: "CLOSE message: subscription ID missed.", + } + } +} + +func (cm Close) EncodeToJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["CLOSE",`) + w.Raw(json.Marshal(string(cm))) + w.RawString(`]`) + + res, err := w.BuildBytes() + if err != nil { + return nil, types.EncodeError{ + Reason: fmt.Sprintf("CLOSE message: %s", err.Error()), + } + } + + return res, nil +} + +// Closed reperesents a NIP-01 CLOSED message. +type Closed struct { + SubscriptionID string + Reason string +} + +// MakeClosed constructs a CLOSED message to be sent to client. +func MakeClosed(id, reason string) []byte { + cm := Closed{ + SubscriptionID: id, + Reason: reason, + } + + res, err := cm.EncodeToJSON() + if err != nil { + return []byte{} + } + + return res +} + +func (Closed) Label() string { return "CLOSED" } +func (cm Closed) String() string { + v, err := json.Marshal(cm) + if err != nil { + return "" + } + + return string(v) +} + +func (cm *Closed) DecodeFromJSON(_ []byte) error { + return nil +} + +func (cm Closed) EncodeToJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["CLOSED",`) + w.Raw(json.Marshal(cm.SubscriptionID)) + w.RawString(`,`) + w.Raw(json.Marshal(cm.Reason)) + w.RawString(`]`) + + res, err := w.BuildBytes() + if err != nil { + return nil, types.EncodeError{ + Reason: fmt.Sprintf("CLOSED message: %s", err.Error()), + } + } + + return res, nil +} + +// OK reperesents a NIP-01 OK message. +type OK struct { + OK bool + EventID string + Reason string +} + +// MakeOK constructs a NIP-01 OK message to be sent to the client. +func MakeOK(ok bool, eid, reason string) []byte { + om := OK{ + OK: ok, + EventID: eid, + Reason: reason, + } + + res, err := om.EncodeToJSON() + if err != nil { + return []byte{} // TODO::: should we return anything else here? + } + + return res +} + +func (OK) Type() string { return "OK" } +func (om OK) String() string { + v, err := json.Marshal(om) + if err != nil { + return "" + } + + return string(v) +} + +func (om *OK) DecodeFromJSON(_ []byte) error { + return nil +} + +func (om OK) EncodeToJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["OK","` + om.EventID + `",`) + ok := "false" + if om.OK { + ok = "true" + } + w.RawString(ok) + w.RawString(`,`) + w.Raw(json.Marshal(om.Reason)) + w.RawString(`]`) + + res, err := w.BuildBytes() + if err != nil { + return nil, types.EncodeError{ + Reason: fmt.Sprintf("OK message: %s", err.Error()), + } + } + + return res, nil +} + +// Auth reperesents a NIP-01 AUTH message. +type Auth struct { + Challenge *string + Event *event.Event +} + +func (Auth) Type() string { return "AUTH" } + +func (am Auth) String() string { + v, err := json.Marshal(am) + if err != nil { + return "" + } + + return string(v) +} + +func (am *Auth) DecodeFromJSON(data []byte) error { + r := gjson.ParseBytes(data) + arr := r.Array() + if len(arr) < 2 { + return types.DecodeError{ + Reason: "AUTH message: missing fields.", + } + } + + if arr[1].IsObject() { + err := easyjson.Unmarshal([]byte(arr[1].Raw), am.Event) + if err != nil { + return types.DecodeError{ + Reason: fmt.Sprintf("AUTH message: %s", err.Error()), + } + } + + return nil + } + + am.Challenge = &arr[1].Str + + return nil +} + +func (am Auth) EncodeToJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["AUTH",`) + w.Raw(json.Marshal(*am.Challenge)) + w.RawString(`]`) + + res, err := w.BuildBytes() + if err != nil { + return nil, types.EncodeError{ + Reason: fmt.Sprintf("AUTH message: %s", err.Error()), + } + } + + return res, nil +} diff --git a/types/envelope/envelope_test.go b/types/message/message_test.go similarity index 51% rename from types/envelope/envelope_test.go rename to types/message/message_test.go index 631afa0..66d12d7 100644 --- a/types/envelope/envelope_test.go +++ b/types/message/message_test.go @@ -1,18 +1,19 @@ -package envelope_test +package message_test import ( "testing" "github.com/dezh-tech/immortal/types" - "github.com/dezh-tech/immortal/types/envelope" + "github.com/dezh-tech/immortal/types/event" "github.com/dezh-tech/immortal/types/filter" + "github.com/dezh-tech/immortal/types/message" "github.com/stretchr/testify/assert" ) type testCase struct { Name string Message []byte - ExpectedEnvelope envelope.Envelope + ExpectedEnvelope message.Message } var testCases = []testCase{ @@ -32,14 +33,9 @@ var testCases = []testCase{ ExpectedEnvelope: nil, }, { - Name: "CLOSED envelope", - Message: []byte(`["CLOSED",":1","error: we are broken"]`), - ExpectedEnvelope: &envelope.ClosedEnvelope{SubscriptionID: ":1", Reason: "error: we are broken"}, - }, - { - Name: "REQ envelope", + Name: "REQ message", Message: []byte(`["REQ","million", {"kinds": [1]}, {"kinds": [30023 ], "#d": ["buteko", "batuke"]}]`), - ExpectedEnvelope: &envelope.ReqEnvelope{ + ExpectedEnvelope: &message.Req{ SubscriptionID: "million", Filters: filter.Filters{{Kinds: []types.Kind{1}}, { Kinds: []types.Kind{30023}, @@ -47,12 +43,27 @@ var testCases = []testCase{ }}, }, }, + { + Name: "EVENT message", + Message: []byte(`["EVENT",{"kind":1,"id":"d86745e397de3a3b8c8f15f7a02c6aa6a60213f59b14e1a7093ea286e020423e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1726055814,"tags":[],"content":"test","sig":"ec9f5702c2698ebfa1ce037c31568c7eb420d65d9684f33ba4d3266c82b771f58cf92c44f1e2a710ba71ce3c92bba1253aa7419d124b2a5eed74e9165e868d50"}]`), + ExpectedEnvelope: &message.Event{ + Event: &event.Event{ + ID: "d86745e397de3a3b8c8f15f7a02c6aa6a60213f59b14e1a7093ea286e020423e", + PublicKey: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + CreatedAt: 1726055814, + Kind: types.KindTextNote, + Tags: []types.Tag{}, + Content: "test", + Signature: "ec9f5702c2698ebfa1ce037c31568c7eb420d65d9684f33ba4d3266c82b771f58cf92c44f1e2a710ba71ce3c92bba1253aa7419d124b2a5eed74e9165e868d50", + }, + }, + }, } func TestEnvelope(t *testing.T) { for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { - parsedEnvelope := envelope.ParseMessage(tc.Message) + parsedEnvelope := message.ParseMessage(tc.Message) if tc.ExpectedEnvelope == nil && parsedEnvelope == nil { return diff --git a/types/utils.go b/types/utils.go index a0b9c12..e994456 100644 --- a/types/utils.go +++ b/types/utils.go @@ -24,7 +24,7 @@ func ContainsKind(target Kind, arr []Kind) bool { return false } -// Escaping strings for JSON encoding according to RFC8259. +// EscapeString for JSON encoding according to RFC8259. // Also encloses result in quotation marks "". func EscapeString(dst []byte, s string) []byte { dst = append(dst, '"')