From c81312917736f06447bea66bd2f5be624b1eb3bc Mon Sep 17 00:00:00 2001 From: kehiy Date: Wed, 11 Sep 2024 14:58:38 +0330 Subject: [PATCH] feat(relay): handlers --- relay/relay.go | 100 ++-- relay/subscription.go | 8 - types/envelope/envelope.go | 429 --------------- types/errors.go | 21 + types/event/event.go | 13 +- types/event/event_easyjson.go | 6 +- types/filter/filter.go | 8 +- types/message/message.go | 508 ++++++++++++++++++ .../message_test.go} | 10 +- 9 files changed, 606 insertions(+), 497 deletions(-) delete mode 100644 relay/subscription.go delete mode 100644 types/envelope/envelope.go create mode 100644 types/errors.go create mode 100644 types/message/message.go rename types/{envelope/envelope_test.go => message/message_test.go} (86%) diff --git a/relay/relay.go b/relay/relay.go index 3ed87a2..0409087 100644 --- a/relay/relay.go +++ b/relay/relay.go @@ -2,12 +2,13 @@ package relay import ( "errors" + "fmt" "io" "log" "net/http" - "github.com/dezh-tech/immortal/types/envelope" "github.com/dezh-tech/immortal/types/filter" + "github.com/dezh-tech/immortal/types/message" "golang.org/x/net/websocket" ) @@ -56,24 +57,25 @@ func (r *Relay) readLoop(ws *websocket.Conn) { continue } - env := envelope.ParseMessage(buf[:n]) - if env == nil { + msg := message.ParseMessage(buf[:n]) + if msg == nil { + _, _ = ws.Write(message.MakeNotice("error: can't parse message.")) // TODO::: should we check error? + continue } // TODO::: replace with logger. - log.Printf("received envelope: %s\n", env.String()) + log.Printf("received envelope: %s\n", msg.String()) - // TODO::: NIP-45, NIP-42. - switch env.Label() { + switch msg.Type() { case "REQ": - go r.HandleReq(ws, env) + go r.HandleReq(ws, msg) case "EVENT": - go r.HandleEvent(ws, env) + go r.HandleEvent(ws, msg) case "CLOSE": - go r.HandleClose(ws, env) // should we pass env here? + go r.HandleClose(ws, msg) default: break @@ -81,89 +83,95 @@ func (r *Relay) readLoop(ws *websocket.Conn) { } } -func (r *Relay) HandleReq(ws *websocket.Conn, e envelope.Envelope) { +func (r *Relay) HandleReq(ws *websocket.Conn, m message.Message) { // TODO::: loadfrom database and sent in first query based on limit. - // see: NIP-01. + // TODO::: return EOSE. // TODO::: use a concurrent safe map. - env, ok := e.(*envelope.ReqEnvelope) + msg, ok := m.(*message.Req) if !ok { - return // TODO::: return EVENT message. + _, _ = ws.Write(message.MakeNotice("error: can't parse REQ message")) // TODO::: should we check error? + return } subs, ok := r.conns[ws] if !ok { - return // TODO::: return EVENT message. + _, _ = ws.Write(message.MakeNotice(fmt.Sprintf("error: can't find connection %s", ws.RemoteAddr()))) // TODO::: should we check error? + return } - subs[env.SubscriptionID] = env.Filters + subs[msg.SubscriptionID] = msg.Filters - // TODO::: return EVENT message. + // TODO::: return EVENT messages. } -func (r *Relay) HandleEvent(ws *websocket.Conn, e envelope.Envelope) { +func (r *Relay) HandleEvent(ws *websocket.Conn, m message.Message) { // TODO::: send events to be stored and proccessed. // can we ignore assertion check? - env, ok := e.(*envelope.EventEnvelope) + msg, ok := m.(*message.Event) if !ok { - res, _ := envelope.MakeOKEnvelope(false, + okm := message.MakeOK(false, "", "error: can't parse the message.", // TODO::: make an error builder. - ).MarshalJSON() + ) - _, _ = ws.Write(res) + _, _ = ws.Write(okm) // TODO::: should we check error? return } - if !env.Event.IsValid() { - res, _ := envelope.MakeOKEnvelope(false, - env.SubscriptionID, - "invalid: invalid _id_ or _sig_.", // TODO::: make an error builder. - ).MarshalJSON() + if !msg.Event.IsValid() { + okm := message.MakeOK(false, + msg.SubscriptionID, + "invalid: invalid id or sig.", // TODO::: make an error builder. + ) - _, _ = ws.Write(res) + _, _ = ws.Write(okm) // TODO::: should we check error? return } - res, _ := envelope.MakeOKEnvelope(true, env.SubscriptionID, "").MarshalJSON() - _, _ = ws.Write(res) + _, _ = ws.Write(message.MakeOK(true, msg.SubscriptionID, "")) // TODO::: should we check error? // TODO::: any better way? for conn, subs := range r.conns { for id, filters := range subs { - if !filters.Match(env.Event) { - continue - } - resEnv := envelope.MakeEventEnvelope(id, env.Event) - encodedResEnv, err := resEnv.MarshalJSON() - if err != nil { - continue - } - - _, err = conn.Write(encodedResEnv) - if err != nil { + if !filters.Match(msg.Event) { continue } + _, _ = conn.Write(message.MakeEvent(id, msg.Event)) // TODO::: should we check error? } } } -func (r *Relay) HandleClose(ws *websocket.Conn, e envelope.Envelope) { - env, ok := e.(*envelope.CloseEnvelope) +func (r *Relay) HandleClose(ws *websocket.Conn, m message.Message) { + msg, ok := m.(*message.Close) if !ok { - // TODO::: send NOTICE message. + _, _ = ws.Write(message.MakeNotice("error: can't parse CLOSE message")) // TODO::: should we check error? + return } conn, ok := r.conns[ws] if !ok { - // TODO::: send NOTICE message. + _, _ = ws.Write(message.MakeNotice(fmt.Sprintf("error: can't find connection %s", ws.RemoteAddr()))) // TODO::: should we check error? + return } - delete(conn, env.String()) - // TODO::: what should we return here? + delete(conn, msg.String()) + _, _ = ws.Write(message.MakeClosed(msg.String(), "ok: closed successfully")) // TODO::: should we check error? +} + +// Stop shutdowns the relay gracefully. +func (r *Relay) Stop() error { + for wsConn, subs := range r.conns { + for id := range subs { + _, _ = wsConn.Write(message.MakeClosed(id, "relay is stopping.")) // TODO::: should we check error? + } + _ = wsConn.Close() // TODO::: should we check error? + } + + return nil } diff --git a/relay/subscription.go b/relay/subscription.go deleted file mode 100644 index 849f8ea..0000000 --- a/relay/subscription.go +++ /dev/null @@ -1,8 +0,0 @@ -package relay - -import "github.com/dezh-tech/immortal/types/filter" - -type Subscription struct { - ID string - Filters filter.Filters -} diff --git a/types/envelope/envelope.go b/types/envelope/envelope.go deleted file mode 100644 index 89d483c..0000000 --- a/types/envelope/envelope.go +++ /dev/null @@ -1,429 +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! -) - -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) -) - -// ParseMessage parses the given message from client to an envelope interface. -// Envelope is the interface. -func ParseMessage(message []byte) Envelope { - firstComma := bytes.Index(message, []byte{','}) - if firstComma == -1 { - return nil - } - label := message[0:firstComma] - - var e Envelope - switch { - case bytes.Contains(label, []byte("EVENT")): - e = &EventEnvelope{} - case bytes.Contains(label, []byte("REQ")): - e = &ReqEnvelope{} - case bytes.Contains(label, []byte("COUNT")): - e = &CountEnvelope{} - case bytes.Contains(label, []byte("AUTH")): - e = &AuthEnvelope{} - case bytes.Contains(label, []byte("CLOSE")): - x := CloseEnvelope("") - e = &x - default: - return nil - } - - if err := e.UnmarshalJSON(message); err != nil { - return nil - } - - return e -} - -type Envelope interface { - Label() string - UnmarshalJSON([]byte) error - MarshalJSON() ([]byte, error) - String() string -} - -type EventEnvelope struct { - SubscriptionID string - Event *event.Event -} - -func MakeEventEnvelope(id string, e *event.Event) EventEnvelope { - return EventEnvelope{ - SubscriptionID: id, - Event: e, - } -} - -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(_ []byte) error { - 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(_ []byte) error { - 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 { - return string(ce) -} - -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(_ []byte) error { - return nil -} - -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 { - OK bool - EventID string - Reason string -} - -func MakeOKEnvelope(ok bool, eid, reason string) OKEnvelope { - return OKEnvelope{ - OK: ok, - EventID: eid, - Reason: reason, - } -} - -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(_ []byte) error { - 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..99cbc53 --- /dev/null +++ b/types/errors.go @@ -0,0 +1,21 @@ +package types + +import "fmt" + +// ErrEncode represents an encoding error. +type ErrEncode struct { + Reason string +} + +func (e ErrEncode) Error() string { + return fmt.Sprintf("encoding error: %s", e.Reason) +} + +// ErrDecode represents an decoding error. +type ErrDecode struct { + Reason string +} + +func (e ErrDecode) Error() string { + return fmt.Sprintf("decoding error: %s", e.Reason) +} diff --git a/types/event/event.go b/types/event/event.go index a611331..ed378a4 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.ErrDecode{ + 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.ErrEncode{ + 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, ',') @@ -96,6 +100,7 @@ func (e *Event) IsValid() bool { return sig.Verify(hash[:], pubkey) } +// String returns and encoded string representation of event e. func (e *Event) String() string { ee, err := e.Encode() if err != nil { 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..2215e1f 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.ErrDecode{ + 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.ErrEncode{ + Reason: err.Error(), + } } return ee, nil diff --git a/types/message/message.go b/types/message/message.go new file mode 100644 index 0000000..dc6763f --- /dev/null +++ b/types/message/message.go @@ -0,0 +1,508 @@ +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{} + 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.ErrDecode{ + Reason: fmt.Sprintf("EVENT message: %s", err.Error()), + } + } + + return nil + default: + + return types.ErrDecode{ + 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.ErrEncode{ + 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.ErrDecode{ + 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.ErrDecode{ + 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.ErrDecode{ + Reason: "COUNT message: missing filters.", + } + } + cm.SubscriptionID = arr[1].Str + + if len(arr) < 3 { + return types.ErrDecode{ + 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.ErrDecode{ + 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.ErrEncode{ + 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.ErrEncode{ + 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.ErrEncode{ + 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.ErrDecode{ + 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.ErrEncode{ + 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.ErrEncode{ + 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.ErrEncode{ + 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.ErrDecode{ + Reason: "AUTH message: missing fields.", + } + } + + if arr[1].IsObject() { + err := easyjson.Unmarshal([]byte(arr[1].Raw), am.Event) + if err != nil { + return types.ErrDecode{ + 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.ErrEncode{ + 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 86% rename from types/envelope/envelope_test.go rename to types/message/message_test.go index 7e8db72..c229c6c 100644 --- a/types/envelope/envelope_test.go +++ b/types/message/message_test.go @@ -1,18 +1,18 @@ -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/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{ @@ -34,7 +34,7 @@ var testCases = []testCase{ { Name: "REQ envelope", 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,7 +47,7 @@ var testCases = []testCase{ 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