diff --git a/api/messaging/spec.json b/api/messaging/spec.json index 0ff6d0a40..5d646b263 100644 --- a/api/messaging/spec.json +++ b/api/messaging/spec.json @@ -372,6 +372,36 @@ ] } }, + { + "name": "executeCommand", + "path": "/command/{command}/exec", + "method": "POST", + "title": "Execute command", + "parameters": { + "path": [ + { + "name": "command", + "type": "string", + "required": true, + "title": "Command to be executed" + } + ], + "post": [ + { + "type": "string", + "name": "input", + "required": false, + "title": "Arbitrary command input" + }, + { + "type": "[]string", + "name": "params", + "required": false, + "title": "Command parameters" + } + ] + } + }, { "name": "history", "path": "/", diff --git a/api/messaging/spec/message.json b/api/messaging/spec/message.json index 84ec828b3..8ca4bcd1c 100644 --- a/api/messaging/spec/message.json +++ b/api/messaging/spec/message.json @@ -35,6 +35,36 @@ ] } }, + { + "Name": "executeCommand", + "Method": "POST", + "Title": "Execute command", + "Path": "/command/{command}/exec", + "Parameters": { + "path": [ + { + "name": "command", + "required": true, + "title": "Command to be executed", + "type": "string" + } + ], + "post": [ + { + "name": "input", + "required": false, + "title": "Arbitrary command input", + "type": "string" + }, + { + "name": "params", + "required": false, + "title": "Command parameters", + "type": "[]string" + } + ] + } + }, { "Name": "history", "Method": "GET", diff --git a/docs/messaging/README.md b/docs/messaging/README.md index b79d12a43..bbfc196c4 100644 --- a/docs/messaging/README.md +++ b/docs/messaging/README.md @@ -300,6 +300,7 @@ The following event types may be sent with a message event: | Method | Endpoint | Purpose | | ------ | -------- | ------- | | `POST` | `/channels/{channelID}/messages/` | Post new message to the channel | +| `POST` | `/channels/{channelID}/messages/command/{command}/exec` | Execute command | | `GET` | `/channels/{channelID}/messages/` | All messages (channel history) | | `GET` | `/channels/{channelID}/messages/mark-as-read` | Manages read/unread messages in a channel or a thread | | `PUT` | `/channels/{channelID}/messages/{messageID}` | Edit existing message | @@ -328,6 +329,23 @@ The following event types may be sent with a message event: | message | string | POST | Message contents (markdown) | N/A | YES | | channelID | uint64 | PATH | Channel ID | N/A | YES | +## Execute command + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/channels/{channelID}/messages/command/{command}/exec` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| command | string | PATH | Command to be executed | N/A | YES | +| channelID | uint64 | PATH | Channel ID | N/A | YES | +| input | string | POST | Arbitrary command input | N/A | NO | +| params | []string | POST | Command parameters | N/A | NO | + ## All messages (channel history) #### Method diff --git a/internal/payload/incoming/command.go b/internal/payload/incoming/command.go deleted file mode 100644 index 54b90f7ca..000000000 --- a/internal/payload/incoming/command.go +++ /dev/null @@ -1,10 +0,0 @@ -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 3f890948c..7fa79bb7e 100644 --- a/internal/payload/incoming/payload.go +++ b/internal/payload/incoming/payload.go @@ -25,6 +25,4 @@ type Payload struct { *MessageDelete `json:"deleteMessage"` *Users `json:"getUsers"` - - *ExecCommand `json:"exec"` } diff --git a/messaging/rest/commands.go b/messaging/rest/commands.go index 80efc42b6..170059a8c 100644 --- a/messaging/rest/commands.go +++ b/messaging/rest/commands.go @@ -22,8 +22,8 @@ func (Commands) New() *Commands { func (ctrl *Commands) List(ctx context.Context, r *request.CommandsList) (interface{}, error) { return types.CommandSet{ &types.Command{ - Name: "echo", - Description: "It does exactly what it says on the tin"}, + Name: "me", + Description: "Illeism"}, &types.Command{ Name: "shrug", Description: "It does exactly what it says on the tin"}, diff --git a/messaging/rest/handlers/message.go b/messaging/rest/handlers/message.go index 829b54c16..331d8a30d 100644 --- a/messaging/rest/handlers/message.go +++ b/messaging/rest/handlers/message.go @@ -29,6 +29,7 @@ import ( // Internal API interface type MessageAPI interface { Create(context.Context, *request.MessageCreate) (interface{}, error) + ExecuteCommand(context.Context, *request.MessageExecuteCommand) (interface{}, error) History(context.Context, *request.MessageHistory) (interface{}, error) MarkAsRead(context.Context, *request.MessageMarkAsRead) (interface{}, error) Edit(context.Context, *request.MessageEdit) (interface{}, error) @@ -46,6 +47,7 @@ type MessageAPI interface { // HTTP API interface type Message struct { Create func(http.ResponseWriter, *http.Request) + ExecuteCommand func(http.ResponseWriter, *http.Request) History func(http.ResponseWriter, *http.Request) MarkAsRead func(http.ResponseWriter, *http.Request) Edit func(http.ResponseWriter, *http.Request) @@ -69,6 +71,13 @@ func NewMessage(mh MessageAPI) *Message { return mh.Create(r.Context(), params) }) }, + ExecuteCommand: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewMessageExecuteCommand() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return mh.ExecuteCommand(r.Context(), params) + }) + }, History: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewMessageHistory() @@ -160,6 +169,7 @@ func (mh *Message) MountRoutes(r chi.Router, middlewares ...func(http.Handler) h r.Group(func(r chi.Router) { r.Use(middlewares...) r.Post("/channels/{channelID}/messages/", mh.Create) + r.Post("/channels/{channelID}/messages/command/{command}/exec", mh.ExecuteCommand) r.Get("/channels/{channelID}/messages/", mh.History) r.Get("/channels/{channelID}/messages/mark-as-read", mh.MarkAsRead) r.Put("/channels/{channelID}/messages/{messageID}", mh.Edit) diff --git a/messaging/rest/message.go b/messaging/rest/message.go index 36c775f95..49c42dae5 100644 --- a/messaging/rest/message.go +++ b/messaging/rest/message.go @@ -65,6 +65,35 @@ func (ctrl *Message) Edit(ctx context.Context, r *request.MessageEdit) (interfac })) } +func (ctrl Message) ExecuteCommand(ctx context.Context, r *request.MessageExecuteCommand) (interface{}, error) { + switch r.Command { + case "me": + if r.Input != "" { + return ctrl.svc.msg.With(ctx).Create(&types.Message{ + Type: types.MessageTypeIlleism, + ChannelID: r.ChannelID, + Message: r.Input, + }) + } + + return nil, nil + + case "shrug": + msg := &types.Message{ + ChannelID: r.ChannelID, + Message: `¯\\_(ツ)_/¯`, + } + + if r.Input != "" { + msg.Message = r.Input + " " + msg.Message + } + + return ctrl.svc.msg.With(ctx).Create(msg) + } + + return nil, errors.New("unknown command") +} + func (ctrl *Message) Delete(ctx context.Context, r *request.MessageDelete) (interface{}, error) { return nil, ctrl.svc.msg.With(ctx).Delete(r.MessageID) } diff --git a/messaging/rest/request/message.go b/messaging/rest/request/message.go index 5181d5b20..2736257ad 100644 --- a/messaging/rest/request/message.go +++ b/messaging/rest/request/message.go @@ -78,6 +78,57 @@ func (mReq *MessageCreate) Fill(r *http.Request) (err error) { var _ RequestFiller = NewMessageCreate() +// Message executeCommand request parameters +type MessageExecuteCommand struct { + Command string + ChannelID uint64 `json:",string"` + Input string + Params []string +} + +func NewMessageExecuteCommand() *MessageExecuteCommand { + return &MessageExecuteCommand{} +} + +func (mReq *MessageExecuteCommand) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(mReq) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + mReq.Command = chi.URLParam(r, "command") + mReq.ChannelID = parseUInt64(chi.URLParam(r, "channelID")) + if val, ok := post["input"]; ok { + + mReq.Input = val + } + + return err +} + +var _ RequestFiller = NewMessageExecuteCommand() + // Message history request parameters type MessageHistory struct { LastMessageID uint64 `json:",string"` diff --git a/messaging/types/message.go b/messaging/types/message.go index 324746e15..65cdc00c1 100644 --- a/messaging/types/message.go +++ b/messaging/types/message.go @@ -82,6 +82,7 @@ const ( MessageTypeChannelEvent = "channelEvent" MessageTypeInlineImage = "inlineImage" MessageTypeAttachment = "attachment" + MessageTypeIlleism = "illeism" ) func (mtype MessageType) String() string { diff --git a/messaging/websocket/session_incoming.go b/messaging/websocket/session_incoming.go index 60524a9f5..66bfd4f90 100644 --- a/messaging/websocket/session_incoming.go +++ b/messaging/websocket/session_incoming.go @@ -49,9 +49,6 @@ 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/messaging/websocket/session_incoming_command.go b/messaging/websocket/session_incoming_command.go deleted file mode 100644 index e3c37410e..000000000 --- a/messaging/websocket/session_incoming_command.go +++ /dev/null @@ -1,54 +0,0 @@ -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/messaging/types" - systemService "github.com/crusttech/crust/system/service" -) - -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 := systemService.User(ctx).FindByID(s.user.Identity()) - if err != nil { - return err - } - - return s.sendReply(&outgoing.Message{ - ID: 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 -}