MM-12843 Add interactive dialogs (#9816)

* Add interactive dialogs

* Fix unit test

* Updates per feedback

* Fix typo

* Updates per feedback, add icon_url and error returns

* Updates per feedback

* Update per feedback
Этот коммит содержится в:
Joram Wilander
2018-11-19 15:27:17 -05:00
коммит произвёл GitHub
родитель 7a6f957638
Коммит 8cfca681b0
23 изменённых файлов: 1286 добавлений и 516 удалений

Просмотреть файл

@@ -162,6 +162,13 @@ func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *
message := strings.Join(parts[1:], " ")
provider := GetCommandProvider(trigger)
clientTriggerId, triggerId, appErr := model.GenerateTriggerId(args.UserId, a.AsymmetricSigningKey())
if appErr != nil {
mlog.Error(appErr.Error())
}
args.TriggerId = triggerId
if provider != nil {
if cmd := provider.GetCommand(a, args.T); cmd != nil {
response := provider.DoCommand(a, args, message)
@@ -174,6 +181,7 @@ func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *
return nil, appErr
}
if cmd != nil {
response.TriggerId = clientTriggerId
return a.HandleCommandResponse(cmd, args, response, true)
}
@@ -228,6 +236,8 @@ func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *
p.Set("command", "/"+trigger)
p.Set("text", message)
p.Set("trigger_id", triggerId)
hook, appErr := a.CreateCommandWebhook(cmd.Id, args)
if appErr != nil {
return nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, appErr.Error(), http.StatusInternalServerError)
@@ -269,6 +279,9 @@ func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *
if response == nil {
return nil, model.NewAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusInternalServerError)
}
response.TriggerId = clientTriggerId
return a.HandleCommandResponse(cmd, args, response, false)
}
}

200
app/integration_action.go Обычный файл
Просмотреть файл

@@ -0,0 +1,200 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
// Integration Action Flow
//
// 1. An integration creates an interactive message button or menu.
// 2. A user clicks on a button or selects an option from the menu.
// 3. The client sends a request to server to complete the post action, calling DoPostAction below.
// 4. DoPostAction will send an HTTP POST request to the integration containing contextual data, including
// an encoded and signed trigger ID. Slash commands also include trigger IDs in their payloads.
// 5. The integration performs any actions it needs to and optionally makes a request back to the MM server
// using the trigger ID to open an interactive dialog.
// 6. If that optional request is made, OpenInteractiveDialog sends a WebSocket event to all connected clients
// for the relevant user, telling them to display the dialog.
// 7. The user fills in the dialog and submits it, where SubmitInteractiveDialog will submit it back to the
// integration for handling.
package app
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/services/httpservice"
"github.com/mattermost/mattermost-server/utils"
)
func (a *App) DoPostAction(postId, actionId, userId, selectedOption string) (string, *model.AppError) {
pchan := a.Srv.Store.Post().GetSingle(postId)
cchan := a.Srv.Store.Channel().GetForPost(postId)
result := <-pchan
if result.Err != nil {
return "", result.Err
}
post := result.Data.(*model.Post)
result = <-cchan
if result.Err != nil {
return "", result.Err
}
channel := result.Data.(*model.Channel)
action := post.GetAction(actionId)
if action == nil || action.Integration == nil {
return "", model.NewAppError("DoPostAction", "api.post.do_action.action_id.app_error", nil, fmt.Sprintf("action=%v", action), http.StatusNotFound)
}
request := &model.PostActionIntegrationRequest{
UserId: userId,
ChannelId: post.ChannelId,
TeamId: channel.TeamId,
PostId: postId,
Type: action.Type,
Context: action.Integration.Context,
}
clientTriggerId, _, err := request.GenerateTriggerId(a.AsymmetricSigningKey())
if err != nil {
return "", err
}
if action.Type == model.POST_ACTION_TYPE_SELECT {
request.DataSource = action.DataSource
request.Context["selected_option"] = selectedOption
}
resp, err := a.DoActionRequest(action.Integration.URL, request.ToJson())
if resp != nil {
defer consumeAndClose(resp)
}
if err != nil {
return "", err
}
var response model.PostActionIntegrationResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return "", model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
}
retainedProps := []string{"override_username", "override_icon_url"}
if response.Update != nil {
response.Update.Id = postId
response.Update.AddProp("from_webhook", "true")
for _, prop := range retainedProps {
if value, ok := post.Props[prop]; ok {
response.Update.Props[prop] = value
} else {
delete(response.Update.Props, prop)
}
}
if _, err := a.UpdatePost(response.Update, false); err != nil {
return "", err
}
}
if response.EphemeralText != "" {
ephemeralPost := &model.Post{}
ephemeralPost.Message = model.ParseSlackLinksToMarkdown(response.EphemeralText)
ephemeralPost.ChannelId = post.ChannelId
ephemeralPost.RootId = post.RootId
if ephemeralPost.RootId == "" {
ephemeralPost.RootId = post.Id
}
ephemeralPost.UserId = post.UserId
ephemeralPost.AddProp("from_webhook", "true")
for _, prop := range retainedProps {
if value, ok := post.Props[prop]; ok {
ephemeralPost.Props[prop] = value
} else {
delete(ephemeralPost.Props, prop)
}
}
a.SendEphemeralPost(userId, ephemeralPost)
}
return clientTriggerId, nil
}
// Perform an HTTP POST request to an integration's action endpoint.
// Caller must consume and close returned http.Response as necessary.
func (a *App) DoActionRequest(rawURL string, body []byte) (*http.Response, *model.AppError) {
req, _ := http.NewRequest("POST", rawURL, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
// Allow access to plugin routes for action buttons
var httpClient *httpservice.Client
url, _ := url.Parse(rawURL)
siteURL, _ := url.Parse(*a.Config().ServiceSettings.SiteURL)
subpath, _ := utils.GetSubpathFromConfig(a.Config())
if (url.Hostname() == "localhost" || url.Hostname() == "127.0.0.1" || url.Hostname() == siteURL.Hostname()) && strings.HasPrefix(url.Path, path.Join(subpath, "plugins")) {
httpClient = a.HTTPService.MakeClient(true)
} else {
httpClient = a.HTTPService.MakeClient(false)
}
resp, httpErr := httpClient.Do(req)
if httpErr != nil {
return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, "err="+httpErr.Error(), http.StatusBadRequest)
}
if resp.StatusCode != http.StatusOK {
return resp, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, fmt.Sprintf("status=%v", resp.StatusCode), http.StatusBadRequest)
}
return resp, nil
}
func (a *App) OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError {
clientTriggerId, userId, err := request.DecodeAndVerifyTriggerId(a.AsymmetricSigningKey())
if err != nil {
return err
}
request.TriggerId = clientTriggerId
jsonRequest, _ := json.Marshal(request)
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_OPEN_DIALOG, "", "", userId, nil)
message.Add("dialog", string(jsonRequest))
a.Publish(message)
return nil
}
func (a *App) SubmitInteractiveDialog(request model.SubmitDialogRequest) (*model.SubmitDialogResponse, *model.AppError) {
url := request.URL
request.URL = ""
request.Type = "dialog_submission"
b, jsonErr := json.Marshal(request)
if jsonErr != nil {
return nil, model.NewAppError("SubmitInteractiveDialog", "app.submit_interactive_dialog.json_error", nil, jsonErr.Error(), http.StatusBadRequest)
}
resp, err := a.DoActionRequest(url, b)
if resp != nil {
defer consumeAndClose(resp)
}
if err != nil {
return nil, err
}
var response model.SubmitDialogResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
// Don't fail, an empty response is acceptable
return &response, nil
}
return &response, nil
}

320
app/integration_action_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,320 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package app
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/model"
)
func TestPostAction(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1"
})
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
request := model.PostActionIntegrationRequestFromJson(r.Body)
assert.NotNil(t, request)
assert.Equal(t, request.UserId, th.BasicUser.Id)
assert.Equal(t, request.ChannelId, th.BasicChannel.Id)
assert.Equal(t, request.TeamId, th.BasicTeam.Id)
assert.True(t, len(request.TriggerId) > 0)
if request.Type == model.POST_ACTION_TYPE_SELECT {
assert.Equal(t, request.DataSource, "some_source")
assert.Equal(t, request.Context["selected_option"], "selected")
} else {
assert.Equal(t, request.DataSource, "")
}
assert.Equal(t, "foo", request.Context["s"])
assert.EqualValues(t, 3, request.Context["n"])
fmt.Fprintf(w, `{"post": {"message": "updated"}, "ephemeral_text": "foo"}`)
}))
defer ts.Close()
interactivePost := model.Post{
Message: "Interactive post",
ChannelId: th.BasicChannel.Id,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"attachments": []*model.SlackAttachment{
{
Text: "hello",
Actions: []*model.PostAction{
{
Integration: &model.PostActionIntegration{
Context: model.StringInterface{
"s": "foo",
"n": 3,
},
URL: ts.URL,
},
Name: "action",
Type: "some_type",
DataSource: "some_source",
},
},
},
},
},
}
post, err := th.App.CreatePostAsUser(&interactivePost, false)
require.Nil(t, err)
attachments, ok := post.Props["attachments"].([]*model.SlackAttachment)
require.True(t, ok)
require.NotEmpty(t, attachments[0].Actions)
require.NotEmpty(t, attachments[0].Actions[0].Id)
menuPost := model.Post{
Message: "Interactive post",
ChannelId: th.BasicChannel.Id,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"attachments": []*model.SlackAttachment{
{
Text: "hello",
Actions: []*model.PostAction{
{
Integration: &model.PostActionIntegration{
Context: model.StringInterface{
"s": "foo",
"n": 3,
},
URL: ts.URL,
},
Name: "action",
Type: model.POST_ACTION_TYPE_SELECT,
DataSource: "some_source",
},
},
},
},
},
}
post2, err := th.App.CreatePostAsUser(&menuPost, false)
require.Nil(t, err)
attachments2, ok := post2.Props["attachments"].([]*model.SlackAttachment)
require.True(t, ok)
require.NotEmpty(t, attachments2[0].Actions)
require.NotEmpty(t, attachments2[0].Actions[0].Id)
clientTriggerId, err := th.App.DoPostAction(post.Id, "notavalidid", th.BasicUser.Id, "")
require.NotNil(t, err)
assert.Equal(t, http.StatusNotFound, err.StatusCode)
assert.True(t, clientTriggerId == "")
clientTriggerId, err = th.App.DoPostAction(post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
require.Nil(t, err)
assert.True(t, len(clientTriggerId) == 26)
clientTriggerId, err = th.App.DoPostAction(post2.Id, attachments2[0].Actions[0].Id, th.BasicUser.Id, "selected")
require.Nil(t, err)
assert.True(t, len(clientTriggerId) == 26)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = ""
})
_, err = th.App.DoPostAction(post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
require.NotNil(t, err)
require.True(t, strings.Contains(err.Error(), "address forbidden"))
interactivePostPlugin := model.Post{
Message: "Interactive post",
ChannelId: th.BasicChannel.Id,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"attachments": []*model.SlackAttachment{
{
Text: "hello",
Actions: []*model.PostAction{
{
Integration: &model.PostActionIntegration{
Context: model.StringInterface{
"s": "foo",
"n": 3,
},
URL: ts.URL + "/plugins/myplugin/myaction",
},
Name: "action",
Type: "some_type",
DataSource: "some_source",
},
},
},
},
},
}
postplugin, err := th.App.CreatePostAsUser(&interactivePostPlugin, false)
require.Nil(t, err)
attachmentsPlugin, ok := postplugin.Props["attachments"].([]*model.SlackAttachment)
require.True(t, ok)
_, err = th.App.DoPostAction(postplugin.Id, attachmentsPlugin[0].Actions[0].Id, th.BasicUser.Id, "")
require.Nil(t, err)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = "http://127.1.1.1"
})
interactivePostSiteURL := model.Post{
Message: "Interactive post",
ChannelId: th.BasicChannel.Id,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"attachments": []*model.SlackAttachment{
{
Text: "hello",
Actions: []*model.PostAction{
{
Integration: &model.PostActionIntegration{
Context: model.StringInterface{
"s": "foo",
"n": 3,
},
URL: "http://127.1.1.1/plugins/myplugin/myaction",
},
Name: "action",
Type: "some_type",
DataSource: "some_source",
},
},
},
},
},
}
postSiteURL, err := th.App.CreatePostAsUser(&interactivePostSiteURL, false)
require.Nil(t, err)
attachmentsSiteURL, ok := postSiteURL.Props["attachments"].([]*model.SlackAttachment)
require.True(t, ok)
_, err = th.App.DoPostAction(postSiteURL.Id, attachmentsSiteURL[0].Actions[0].Id, th.BasicUser.Id, "")
require.NotNil(t, err)
require.False(t, strings.Contains(err.Error(), "address forbidden"))
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = ts.URL + "/subpath"
})
interactivePostSubpath := model.Post{
Message: "Interactive post",
ChannelId: th.BasicChannel.Id,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"attachments": []*model.SlackAttachment{
{
Text: "hello",
Actions: []*model.PostAction{
{
Integration: &model.PostActionIntegration{
Context: model.StringInterface{
"s": "foo",
"n": 3,
},
URL: ts.URL + "/subpath/plugins/myplugin/myaction",
},
Name: "action",
Type: "some_type",
DataSource: "some_source",
},
},
},
},
},
}
postSubpath, err := th.App.CreatePostAsUser(&interactivePostSubpath, false)
require.Nil(t, err)
attachmentsSubpath, ok := postSubpath.Props["attachments"].([]*model.SlackAttachment)
require.True(t, ok)
_, err = th.App.DoPostAction(postSubpath.Id, attachmentsSubpath[0].Actions[0].Id, th.BasicUser.Id, "")
require.Nil(t, err)
}
func TestSubmitInteractiveDialog(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1"
})
submit := model.SubmitDialogRequest{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
TeamId: th.BasicTeam.Id,
CallbackId: "someid",
State: "somestate",
Submission: map[string]interface{}{
"name1": "value1",
},
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request model.SubmitDialogRequest
err := json.NewDecoder(r.Body).Decode(&request)
require.Nil(t, err)
assert.NotNil(t, request)
assert.Equal(t, request.URL, "")
assert.Equal(t, request.UserId, submit.UserId)
assert.Equal(t, request.ChannelId, submit.ChannelId)
assert.Equal(t, request.TeamId, submit.TeamId)
assert.Equal(t, request.CallbackId, submit.CallbackId)
assert.Equal(t, request.State, submit.State)
val, ok := request.Submission["name1"].(string)
require.True(t, ok)
assert.Equal(t, "value1", val)
resp := model.SubmitDialogResponse{
Errors: map[string]string{"name1": "some error"},
}
b, _ := json.Marshal(resp)
w.Write(b)
}))
defer ts.Close()
submit.URL = ts.URL
resp, err := th.App.SubmitInteractiveDialog(submit)
assert.Nil(t, err)
require.NotNil(t, resp)
assert.Equal(t, "some error", resp.Errors["name1"])
submit.URL = ""
resp, err = th.App.SubmitInteractiveDialog(submit)
assert.NotNil(t, err)
assert.Nil(t, resp)
}

Просмотреть файл

@@ -495,6 +495,10 @@ func (api *PluginAPI) SetTeamIcon(teamId string, data []byte) *model.AppError {
return nil
}
func (api *PluginAPI) OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppError {
return api.app.OpenInteractiveDialog(dialog)
}
// Plugin Section
func (api *PluginAPI) GetPlugins() ([]*model.Manifest, *model.AppError) {

Просмотреть файл

@@ -7,12 +7,10 @@ import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
"github.com/dyatlov/go-opengraph/opengraph"
@@ -21,7 +19,6 @@ import (
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/plugin"
"github.com/mattermost/mattermost-server/services/httpservice"
"github.com/mattermost/mattermost-server/store"
"github.com/mattermost/mattermost-server/utils"
)
@@ -862,111 +859,6 @@ func makeOpenGraphURLsAbsolute(og *opengraph.OpenGraph, requestURL string) {
}
}
func (a *App) DoPostAction(postId, actionId, userId, selectedOption string) *model.AppError {
pchan := a.Srv.Store.Post().GetSingle(postId)
cchan := a.Srv.Store.Channel().GetForPost(postId)
result := <-pchan
if result.Err != nil {
return result.Err
}
post := result.Data.(*model.Post)
result = <-cchan
if result.Err != nil {
return result.Err
}
channel := result.Data.(*model.Channel)
action := post.GetAction(actionId)
if action == nil || action.Integration == nil {
return model.NewAppError("DoPostAction", "api.post.do_action.action_id.app_error", nil, fmt.Sprintf("action=%v", action), http.StatusNotFound)
}
request := &model.PostActionIntegrationRequest{
UserId: userId,
ChannelId: post.ChannelId,
TeamId: channel.TeamId,
PostId: postId,
Type: action.Type,
Context: action.Integration.Context,
}
if action.Type == model.POST_ACTION_TYPE_SELECT {
request.DataSource = action.DataSource
request.Context["selected_option"] = selectedOption
}
req, _ := http.NewRequest("POST", action.Integration.URL, strings.NewReader(request.ToJson()))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
// Allow access to plugin routes for action buttons
var httpClient *httpservice.Client
url, _ := url.Parse(action.Integration.URL)
siteURL, _ := url.Parse(*a.Config().ServiceSettings.SiteURL)
subpath, _ := utils.GetSubpathFromConfig(a.Config())
if (url.Hostname() == "localhost" || url.Hostname() == "127.0.0.1" || url.Hostname() == siteURL.Hostname()) && strings.HasPrefix(url.Path, path.Join(subpath, "plugins")) {
httpClient = a.HTTPService.MakeClient(true)
} else {
httpClient = a.HTTPService.MakeClient(false)
}
resp, err := httpClient.Do(req)
if err != nil {
return model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
}
defer consumeAndClose(resp)
if resp.StatusCode != http.StatusOK {
return model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, fmt.Sprintf("status=%v", resp.StatusCode), http.StatusBadRequest)
}
var response model.PostActionIntegrationResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
}
retainedProps := []string{"override_username", "override_icon_url"}
if response.Update != nil {
response.Update.Id = postId
response.Update.AddProp("from_webhook", "true")
for _, prop := range retainedProps {
if value, ok := post.Props[prop]; ok {
response.Update.Props[prop] = value
} else {
delete(response.Update.Props, prop)
}
}
if _, err := a.UpdatePost(response.Update, false); err != nil {
return err
}
}
if response.EphemeralText != "" {
ephemeralPost := &model.Post{}
ephemeralPost.Message = model.ParseSlackLinksToMarkdown(response.EphemeralText)
ephemeralPost.ChannelId = post.ChannelId
ephemeralPost.RootId = post.RootId
if ephemeralPost.RootId == "" {
ephemeralPost.RootId = post.Id
}
ephemeralPost.UserId = post.UserId
ephemeralPost.AddProp("from_webhook", "true")
for _, prop := range retainedProps {
if value, ok := post.Props[prop]; ok {
ephemeralPost.Props[prop] = value
} else {
delete(ephemeralPost.Props, prop)
}
}
a.SendEphemeralPost(userId, ephemeralPost)
}
return nil
}
func (a *App) PostListWithProxyAddedToImageURLs(list *model.PostList) *model.PostList {
if f := a.ImageProxyAdder(); f != nil {
return list.WithRewrittenImageURLs(f)

Просмотреть файл

@@ -6,7 +6,6 @@ package app
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
@@ -120,246 +119,6 @@ func TestPostReplyToPostWhereRootPosterLeftChannel(t *testing.T) {
}
}
func TestPostAction(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1"
})
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
request := model.PostActionIntegrationRequesteFromJson(r.Body)
assert.NotNil(t, request)
assert.Equal(t, request.UserId, th.BasicUser.Id)
assert.Equal(t, request.ChannelId, th.BasicChannel.Id)
assert.Equal(t, request.TeamId, th.BasicTeam.Id)
if request.Type == model.POST_ACTION_TYPE_SELECT {
assert.Equal(t, request.DataSource, "some_source")
assert.Equal(t, request.Context["selected_option"], "selected")
} else {
assert.Equal(t, request.DataSource, "")
}
assert.Equal(t, "foo", request.Context["s"])
assert.EqualValues(t, 3, request.Context["n"])
fmt.Fprintf(w, `{"post": {"message": "updated"}, "ephemeral_text": "foo"}`)
}))
defer ts.Close()
interactivePost := model.Post{
Message: "Interactive post",
ChannelId: th.BasicChannel.Id,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"attachments": []*model.SlackAttachment{
{
Text: "hello",
Actions: []*model.PostAction{
{
Integration: &model.PostActionIntegration{
Context: model.StringInterface{
"s": "foo",
"n": 3,
},
URL: ts.URL,
},
Name: "action",
Type: "some_type",
DataSource: "some_source",
},
},
},
},
},
}
post, err := th.App.CreatePostAsUser(&interactivePost, false)
require.Nil(t, err)
attachments, ok := post.Props["attachments"].([]*model.SlackAttachment)
require.True(t, ok)
require.NotEmpty(t, attachments[0].Actions)
require.NotEmpty(t, attachments[0].Actions[0].Id)
menuPost := model.Post{
Message: "Interactive post",
ChannelId: th.BasicChannel.Id,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"attachments": []*model.SlackAttachment{
{
Text: "hello",
Actions: []*model.PostAction{
{
Integration: &model.PostActionIntegration{
Context: model.StringInterface{
"s": "foo",
"n": 3,
},
URL: ts.URL,
},
Name: "action",
Type: model.POST_ACTION_TYPE_SELECT,
DataSource: "some_source",
},
},
},
},
},
}
post2, err := th.App.CreatePostAsUser(&menuPost, false)
require.Nil(t, err)
attachments2, ok := post2.Props["attachments"].([]*model.SlackAttachment)
require.True(t, ok)
require.NotEmpty(t, attachments2[0].Actions)
require.NotEmpty(t, attachments2[0].Actions[0].Id)
err = th.App.DoPostAction(post.Id, "notavalidid", th.BasicUser.Id, "")
require.NotNil(t, err)
assert.Equal(t, http.StatusNotFound, err.StatusCode)
err = th.App.DoPostAction(post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
require.Nil(t, err)
err = th.App.DoPostAction(post2.Id, attachments2[0].Actions[0].Id, th.BasicUser.Id, "selected")
require.Nil(t, err)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = ""
})
err = th.App.DoPostAction(post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
require.NotNil(t, err)
require.True(t, strings.Contains(err.Error(), "address forbidden"))
interactivePostPlugin := model.Post{
Message: "Interactive post",
ChannelId: th.BasicChannel.Id,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"attachments": []*model.SlackAttachment{
{
Text: "hello",
Actions: []*model.PostAction{
{
Integration: &model.PostActionIntegration{
Context: model.StringInterface{
"s": "foo",
"n": 3,
},
URL: ts.URL + "/plugins/myplugin/myaction",
},
Name: "action",
Type: "some_type",
DataSource: "some_source",
},
},
},
},
},
}
postplugin, err := th.App.CreatePostAsUser(&interactivePostPlugin, false)
require.Nil(t, err)
attachmentsPlugin, ok := postplugin.Props["attachments"].([]*model.SlackAttachment)
require.True(t, ok)
err = th.App.DoPostAction(postplugin.Id, attachmentsPlugin[0].Actions[0].Id, th.BasicUser.Id, "")
require.Nil(t, err)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = "http://127.1.1.1"
})
interactivePostSiteURL := model.Post{
Message: "Interactive post",
ChannelId: th.BasicChannel.Id,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"attachments": []*model.SlackAttachment{
{
Text: "hello",
Actions: []*model.PostAction{
{
Integration: &model.PostActionIntegration{
Context: model.StringInterface{
"s": "foo",
"n": 3,
},
URL: "http://127.1.1.1/plugins/myplugin/myaction",
},
Name: "action",
Type: "some_type",
DataSource: "some_source",
},
},
},
},
},
}
postSiteURL, err := th.App.CreatePostAsUser(&interactivePostSiteURL, false)
require.Nil(t, err)
attachmentsSiteURL, ok := postSiteURL.Props["attachments"].([]*model.SlackAttachment)
require.True(t, ok)
err = th.App.DoPostAction(postSiteURL.Id, attachmentsSiteURL[0].Actions[0].Id, th.BasicUser.Id, "")
require.NotNil(t, err)
require.False(t, strings.Contains(err.Error(), "address forbidden"))
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = ts.URL + "/subpath"
})
interactivePostSubpath := model.Post{
Message: "Interactive post",
ChannelId: th.BasicChannel.Id,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"attachments": []*model.SlackAttachment{
{
Text: "hello",
Actions: []*model.PostAction{
{
Integration: &model.PostActionIntegration{
Context: model.StringInterface{
"s": "foo",
"n": 3,
},
URL: ts.URL + "/subpath/plugins/myplugin/myaction",
},
Name: "action",
Type: "some_type",
DataSource: "some_source",
},
},
},
},
},
}
postSubpath, err := th.App.CreatePostAsUser(&interactivePostSubpath, false)
require.Nil(t, err)
attachmentsSubpath, ok := postSubpath.Props["attachments"].([]*model.SlackAttachment)
require.True(t, ok)
err = th.App.DoPostAction(postSubpath.Id, attachmentsSubpath[0].Actions[0].Id, th.BasicUser.Id, "")
require.Nil(t, err)
}
func TestPostChannelMentions(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()