From f94567c97b04a0237ec4f2a41973130df205c74f Mon Sep 17 00:00:00 2001 From: Michael Kochell Date: Wed, 12 Dec 2018 01:43:31 -0500 Subject: [PATCH] [MM-9937] Add support for sending a message to a different channel than where the slash command was issued from (#9878) * add support for channel_id in a slash command response * add test for HandleCommandResponsePost --- app/command.go | 15 +++- app/command_test.go | 137 ++++++++++++++++++++++++++++++++- i18n/en.json | 4 + model/command_response.go | 1 + model/command_response_test.go | 2 + 5 files changed, 156 insertions(+), 3 deletions(-) diff --git a/app/command.go b/app/command.go index 490bc25ecb..0b43abd56b 100644 --- a/app/command.go +++ b/app/command.go @@ -41,6 +41,13 @@ func (a *App) CreateCommandPost(post *model.Post, teamId string, response *model post.Message = model.ParseSlackLinksToMarkdown(response.Text) post.CreateAt = model.GetMillis() + _, err := a.GetChannelMember(post.ChannelId, post.UserId) + if err != nil { + err = model.NewAppError("CreateCommandPost", "api.command.command_post.forbidden.app_error", nil, err.Error(), http.StatusForbidden) + mlog.Error(err.Error()) + return nil, err + } + if strings.HasPrefix(post.Type, model.POST_SYSTEM_MESSAGE_PREFIX) { err := model.NewAppError("CreateCommandPost", "api.context.invalid_param.app_error", map[string]interface{}{"Name": "post.type"}, "", http.StatusBadRequest) return nil, err @@ -301,7 +308,7 @@ func (a *App) HandleCommandResponse(command *model.Command, args *model.CommandA return response, nil } -func (a *App) HandleCommandResponsePost(command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError) { +func (a *App) HandleCommandResponsePost(command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.Post, *model.AppError) { post := &model.Post{} post.ChannelId = args.ChannelId post.RootId = args.RootId @@ -310,6 +317,10 @@ func (a *App) HandleCommandResponsePost(command *model.Command, args *model.Comm post.Type = response.Type post.Props = response.Props + if len(response.ChannelId) != 0 { + post.ChannelId = response.ChannelId + } + isBotPost := !builtIn if a.Config().ServiceSettings.EnablePostUsernameOverride { @@ -346,7 +357,7 @@ func (a *App) HandleCommandResponsePost(command *model.Command, args *model.Comm mlog.Error(err.Error()) } - return response, nil + return post, nil } func (a *App) CreateCommand(cmd *model.Command) (*model.Command, *model.AppError) { diff --git a/app/command_test.go b/app/command_test.go index de48224369..8594477d69 100644 --- a/app/command_test.go +++ b/app/command_test.go @@ -61,7 +61,142 @@ func TestCreateCommandPost(t *testing.T) { } _, err := th.App.CreateCommandPost(post, th.BasicTeam.Id, resp) - if err == nil && err.Id != "api.context.invalid_param.app_error" { + if err == nil || err.Id != "api.context.invalid_param.app_error" { t.Fatal("should have failed - bad post type") } + + channel := th.CreateChannel(th.BasicTeam) + + post = &model.Post{ + ChannelId: th.BasicChannel.Id, + UserId: channel.Id, + } + + _, err = th.App.CreateCommandPost(post, th.BasicTeam.Id, resp) + if err == nil || err.Id != "api.command.command_post.forbidden.app_error" { + t.Fatal("should have failed - forbidden channel post") + } +} + +func TestHandleCommandResponsePost(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + command := &model.Command{} + args := &model.CommandArgs{ + ChannelId: th.BasicChannel.Id, + TeamId: th.BasicTeam.Id, + UserId: th.BasicUser.Id, + RootId: "root_id", + ParentId: "parent_id", + } + + resp := &model.CommandResponse{ + Type: model.POST_EPHEMERAL, + Props: model.StringInterface{"some_key": "some value"}, + Text: "some message", + } + + builtIn := true + + post, err := th.App.HandleCommandResponsePost(command, args, resp, builtIn) + assert.Nil(t, err) + assert.Equal(t, args.ChannelId, post.ChannelId) + assert.Equal(t, args.RootId, post.RootId) + assert.Equal(t, args.ParentId, post.ParentId) + assert.Equal(t, args.UserId, post.UserId) + assert.Equal(t, resp.Type, post.Type) + assert.Equal(t, resp.Props, post.Props) + assert.Equal(t, resp.Text, post.Message) + assert.Nil(t, post.Props["override_icon_url"]) + assert.Nil(t, post.Props["override_username"]) + assert.Nil(t, post.Props["from_webhook"]) + + // Command is not built in, so it is a bot command. + builtIn = false + post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) + assert.Equal(t, "true", post.Props["from_webhook"]) + + builtIn = true + + // Channel id is specified by response, it should override the command args value. + channel := th.CreateChannel(th.BasicTeam) + resp.ChannelId = channel.Id + th.AddUserToChannel(th.BasicUser, channel) + + post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) + assert.Nil(t, err) + assert.Equal(t, resp.ChannelId, post.ChannelId) + assert.NotEqual(t, args.ChannelId, post.ChannelId) + + // Override username config is turned off. No override should occur. + th.App.Config().ServiceSettings.EnablePostUsernameOverride = false + resp.ChannelId = "" + command.Username = "Command username" + resp.Username = "Response username" + + post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) + assert.Nil(t, err) + assert.Nil(t, post.Props["override_username"]) + + th.App.Config().ServiceSettings.EnablePostUsernameOverride = true + + // Override username config is turned on. Override username through command property. + post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) + assert.Nil(t, err) + assert.Equal(t, command.Username, post.Props["override_username"]) + assert.Equal(t, "true", post.Props["from_webhook"]) + + command.Username = "" + + // Override username through response property. + post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) + assert.Nil(t, err) + assert.Equal(t, resp.Username, post.Props["override_username"]) + assert.Equal(t, "true", post.Props["from_webhook"]) + + th.App.Config().ServiceSettings.EnablePostUsernameOverride = false + + // Override icon url config is turned off. No override should occur. + th.App.Config().ServiceSettings.EnablePostIconOverride = false + command.IconURL = "Command icon url" + resp.IconURL = "Response icon url" + + post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) + assert.Nil(t, err) + assert.Nil(t, post.Props["override_icon_url"]) + + th.App.Config().ServiceSettings.EnablePostIconOverride = true + + // Override icon url config is turned on. Override icon url through command property. + post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) + assert.Nil(t, err) + assert.Equal(t, command.IconURL, post.Props["override_icon_url"]) + assert.Equal(t, "true", post.Props["from_webhook"]) + + command.IconURL = "" + + // Override icon url through response property. + post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) + assert.Nil(t, err) + assert.Equal(t, resp.IconURL, post.Props["override_icon_url"]) + assert.Equal(t, "true", post.Props["from_webhook"]) + + // Test Slack text conversion. + resp.Text = "" + + post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) + assert.Nil(t, err) + assert.Equal(t, "@channel", post.Message) + + // Test Slack attachments text conversion. + resp.Attachments = []*model.SlackAttachment{ + &model.SlackAttachment{ + Text: "", + }, + } + + post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) + assert.Nil(t, err) + assert.Equal(t, "@here", resp.Attachments[0].Text) } diff --git a/i18n/en.json b/i18n/en.json index 905cc575bd..c45040557a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -383,6 +383,10 @@ "id": "api.command.execute_command.start.app_error", "translation": "No command trigger found" }, + { + "id": "api.command.command_post.forbidden.app_error", + "translation": "Specified user is not a member of specified channel." + }, { "id": "api.command.invite_people.desc", "translation": "Send an email invite to your Mattermost team" diff --git a/model/command_response.go b/model/command_response.go index 2f6cd0d3f3..6335cef0d1 100644 --- a/model/command_response.go +++ b/model/command_response.go @@ -21,6 +21,7 @@ type CommandResponse struct { ResponseType string `json:"response_type"` Text string `json:"text"` Username string `json:"username"` + ChannelId string `json:"channel_id"` IconURL string `json:"icon_url"` Type string `json:"type"` Props StringInterface `json:"props"` diff --git a/model/command_response_test.go b/model/command_response_test.go index ae941048ea..071cd4129a 100644 --- a/model/command_response_test.go +++ b/model/command_response_test.go @@ -65,6 +65,7 @@ func TestCommandResponseFromJson(t *testing.T) { "response_type": "ephemeral", "text": "response text", "username": "response username", + "channel_id": "response channel id", "icon_url": "response icon url", "goto_location": "response goto location", "attachments": [{ @@ -87,6 +88,7 @@ func TestCommandResponseFromJson(t *testing.T) { ResponseType: "ephemeral", Text: "response text", Username: "response username", + ChannelId: "response channel id", IconURL: "response icon url", GotoLocation: "response goto location", Attachments: []*SlackAttachment{