From cbc83cb515431447208ef8327a4a3498f4903efb Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Thu, 4 Oct 2018 18:02:33 +0200 Subject: [PATCH] Add support for commands --- internal/payload/incoming/command.go | 10 +++ internal/payload/incoming/payload.go | 2 + internal/payload/outgoing.go | 20 ++++++ internal/payload/outgoing/command.go | 18 ++++++ internal/payload/outgoing/payload.go | 2 + sam/types/command.go | 77 +++++++++++++++++++++++ sam/websocket/session.go | 2 + sam/websocket/session_incoming.go | 3 + sam/websocket/session_incoming_command.go | 53 ++++++++++++++++ 9 files changed, 187 insertions(+) create mode 100644 internal/payload/incoming/command.go create mode 100644 internal/payload/outgoing/command.go create mode 100644 sam/types/command.go create mode 100644 sam/websocket/session_incoming_command.go diff --git a/internal/payload/incoming/command.go b/internal/payload/incoming/command.go new file mode 100644 index 000000000..54b90f7ca --- /dev/null +++ b/internal/payload/incoming/command.go @@ -0,0 +1,10 @@ +package incoming + +type ( + ExecCommand struct { + ChannelID string `json:"channelId"` + Command string `json:"command"` + Params map[string]string `json:"params"` + Input string `json:"input"` + } +) diff --git a/internal/payload/incoming/payload.go b/internal/payload/incoming/payload.go index eff7d3348..4a8d8204a 100644 --- a/internal/payload/incoming/payload.go +++ b/internal/payload/incoming/payload.go @@ -19,4 +19,6 @@ type Payload struct { *MessageDelete `json:"deleteMessage"` *Users `json:"getUsers"` + + *ExecCommand `json:"exec"` } diff --git a/internal/payload/outgoing.go b/internal/payload/outgoing.go index 0a626886e..135eaeb99 100644 --- a/internal/payload/outgoing.go +++ b/internal/payload/outgoing.go @@ -115,3 +115,23 @@ func Attachment(in *sam.Attachment) *outgoing.Attachment { UpdatedAt: in.UpdatedAt, } } + +func Command(cmd *sam.Command) *outgoing.Command { + if cmd == nil { + return nil + } + + return &outgoing.Command{ + Name: cmd.Name, + Description: cmd.Description, + } +} + +func Commands(cc sam.CommandSet) *outgoing.CommandSet { + out := make([]*outgoing.Command, len(cc)) + for k, m := range cc { + out[k] = Command(m) + } + retval := outgoing.CommandSet(out) + return &retval +} diff --git a/internal/payload/outgoing/command.go b/internal/payload/outgoing/command.go new file mode 100644 index 000000000..a677dbbf9 --- /dev/null +++ b/internal/payload/outgoing/command.go @@ -0,0 +1,18 @@ +package outgoing + +import ( + "encoding/json" +) + +type ( + Command struct { + Name string `json:"name"` + Description string `json:"description"` + } + + CommandSet []*Command +) + +func (p *CommandSet) EncodeMessage() ([]byte, error) { + return json.Marshal(Payload{CommandSet: p}) +} diff --git a/internal/payload/outgoing/payload.go b/internal/payload/outgoing/payload.go index 7776c5d2c..f13c42cae 100644 --- a/internal/payload/outgoing/payload.go +++ b/internal/payload/outgoing/payload.go @@ -18,6 +18,8 @@ type ( *User `json:"user,omitempty"` *UserSet `json:"users,omitempty"` + + *CommandSet `json:"commands,omitempty"` } // This is same-same but different as using the json.Marshaler diff --git a/sam/types/command.go b/sam/types/command.go new file mode 100644 index 000000000..6eb0db305 --- /dev/null +++ b/sam/types/command.go @@ -0,0 +1,77 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + + "github.com/pkg/errors" +) + +type ( + Command struct { + Name string `db:"name" json:"name"` + Params CommandParamSet `db:"params" json:"params"` + Description string `db:"description" json:"description"` + } + + CommandParam struct { + Name string `db:"name" json:"name"` + Type string `db:"type" json:"type"` + Required bool `db:"required" json:"required"` + } + + CommandSet []*Command + CommandParamSet []*CommandParam +) + +var ( + Preset CommandSet // @todo move this to someplace safe +) + +func init() { + Preset = CommandSet{ + &Command{ + Name: "echo", + Description: "It does exactly what it says on the tin"}, + &Command{ + Name: "shrug", + Description: "It does exactly what it says on the tin"}, + } +} + +func (cc CommandSet) Walk(w func(*Command) error) (err error) { + for i := range cc { + if err = w(cc[i]); err != nil { + return + } + } + + return +} + +func (pp CommandParamSet) Walk(w func(*CommandParam) error) (err error) { + for i := range pp { + if err = w(pp[i]); err != nil { + return + } + } + + return +} + +func (pp *CommandParamSet) Scan(value interface{}) error { + switch value.(type) { + case nil: + *pp = CommandParamSet{} + case []uint8: + if err := json.Unmarshal(value.([]byte), pp); err != nil { + return errors.Wrapf(err, "Can not scan '%v' into CommandParamSet", value) + } + } + + return nil +} + +func (pp CommandParamSet) Value() (driver.Value, error) { + return json.Marshal(pp) +} diff --git a/sam/websocket/session.go b/sam/websocket/session.go index 9c53af467..7aa92f9eb 100644 --- a/sam/websocket/session.go +++ b/sam/websocket/session.go @@ -87,6 +87,8 @@ func (sess *Session) connected() { }) } + sess.sendReply(payload.Commands(types.Preset)) + // Tell everyone that user has connected sess.sendToAll(&outgoing.Connected{UserID: payload.Uint64toa(sess.user.Identity())}) } diff --git a/sam/websocket/session_incoming.go b/sam/websocket/session_incoming.go index 055f478c8..ac82b649c 100644 --- a/sam/websocket/session_incoming.go +++ b/sam/websocket/session_incoming.go @@ -41,6 +41,9 @@ func (s *Session) dispatch(raw []byte) error { case p.Users != nil: return s.userList(ctx, p.Users) + + case p.ExecCommand != nil: + return s.execCommand(ctx, p.ExecCommand) } return nil diff --git a/sam/websocket/session_incoming_command.go b/sam/websocket/session_incoming_command.go new file mode 100644 index 000000000..d5a22e914 --- /dev/null +++ b/sam/websocket/session_incoming_command.go @@ -0,0 +1,53 @@ +package websocket + +import ( + "context" + "log" + "time" + + "github.com/titpetric/factory" + + "github.com/crusttech/crust/internal/payload" + "github.com/crusttech/crust/internal/payload/incoming" + "github.com/crusttech/crust/internal/payload/outgoing" + "github.com/crusttech/crust/sam/types" +) + +func (s *Session) execCommand(ctx context.Context, c *incoming.ExecCommand) error { + // @todo: check access / can we join this channel (should be done by service layer) + + log.Printf("Received command '%s(%v)", c.Command, c.Params) + + switch c.Command { + case "echo": + if c.Input != "" { + user, err := s.svc.user.With(ctx).FindByID(s.user.Identity()) + if err != nil { + return err + } + + return s.sendReply(&outgoing.Message{ + ID: payload.Uint64toa(factory.Sonyflake.NextID()), + User: payload.User(user), + CreatedAt: time.Now(), + Type: "hallucination", + ChannelID: c.ChannelID, + Message: c.Input}) + } + + case "shrug": + msg := &types.Message{ + ChannelID: payload.ParseUInt64(c.ChannelID), + Message: "¯\\_(ツ)_/¯", + } + + if c.Input != "" { + msg.Message = c.Input + " " + msg.Message + } + _, err := s.svc.msg.With(ctx).Create(msg) + + return err + } + + return nil +}