Merge branch 'master' into post-metadata
Этот коммит содержится в:
@@ -4,6 +4,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -747,3 +748,46 @@ func TestGetChannelMembersTimezones(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, 2, len(timezones))
|
||||
}
|
||||
|
||||
func TestGetPublicChannelsForTeam(t *testing.T) {
|
||||
th := Setup()
|
||||
team := th.CreateTeam()
|
||||
defer th.TearDown()
|
||||
|
||||
var expectedChannels []*model.Channel
|
||||
|
||||
townSquare, err := th.App.GetChannelByName("town-square", team.Id, false)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, townSquare)
|
||||
expectedChannels = append(expectedChannels, townSquare)
|
||||
|
||||
offTopic, err := th.App.GetChannelByName("off-topic", team.Id, false)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, offTopic)
|
||||
expectedChannels = append(expectedChannels, offTopic)
|
||||
|
||||
for i := 0; i < 8; i++ {
|
||||
channel := model.Channel{
|
||||
DisplayName: fmt.Sprintf("Public %v", i),
|
||||
Name: fmt.Sprintf("public_%v", i),
|
||||
Type: model.CHANNEL_OPEN,
|
||||
TeamId: team.Id,
|
||||
}
|
||||
rchannel, err := th.App.CreateChannel(&channel, false)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, rchannel)
|
||||
defer th.App.PermanentDeleteChannel(rchannel)
|
||||
|
||||
// Store the user ids for comparison later
|
||||
expectedChannels = append(expectedChannels, rchannel)
|
||||
}
|
||||
|
||||
// Fetch public channels multipile times
|
||||
channelList, err := th.App.GetPublicChannelsForTeam(team.Id, 0, 5)
|
||||
require.Nil(t, err)
|
||||
channelList2, err := th.App.GetPublicChannelsForTeam(team.Id, 5, 5)
|
||||
require.Nil(t, err)
|
||||
|
||||
channels := append(*channelList, *channelList2...)
|
||||
assert.ElementsMatch(t, expectedChannels, channels)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -277,6 +290,18 @@ func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *
|
||||
}
|
||||
|
||||
func (a *App) HandleCommandResponse(command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError) {
|
||||
a.HandleCommandResponsePost(command, args, response, builtIn)
|
||||
|
||||
if response.ExtraResponses != nil {
|
||||
for _, resp := range response.ExtraResponses {
|
||||
a.HandleCommandResponsePost(command, args, resp, builtIn)
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (a *App) HandleCommandResponsePost(command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError) {
|
||||
post := &model.Post{}
|
||||
post.ChannelId = args.ChannelId
|
||||
post.RootId = args.RootId
|
||||
|
||||
@@ -5,14 +5,17 @@ package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
func (a *App) BulkExport(writer io.Writer) *model.AppError {
|
||||
func (a *App) BulkExport(writer io.Writer, file string, pathToEmojiDir string, dirNameToExportEmoji string) *model.AppError {
|
||||
if err := a.ExportVersion(writer); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -32,6 +35,9 @@ func (a *App) BulkExport(writer io.Writer) *model.AppError {
|
||||
if err := a.ExportAllPosts(writer); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.ExportCustomEmoji(writer, file, pathToEmojiDir, dirNameToExportEmoji); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -338,3 +344,88 @@ func (a *App) BuildPostReactions(postId string) (*[]ReactionImportData, *model.A
|
||||
return &reactionsOfPost, nil
|
||||
|
||||
}
|
||||
|
||||
func (a *App) ExportCustomEmoji(writer io.Writer, file string, pathToEmojiDir string, dirNameToExportEmoji string) *model.AppError {
|
||||
pageNumber := 0
|
||||
for {
|
||||
customEmojiList, err := a.GetEmojiList(pageNumber, 100, model.EMOJI_SORT_BY_NAME)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(customEmojiList) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
pageNumber++
|
||||
|
||||
pathToDir := a.createDirForEmoji(file, dirNameToExportEmoji)
|
||||
|
||||
for _, emoji := range customEmojiList {
|
||||
emojiImagePath := pathToEmojiDir + emoji.Id + "/image"
|
||||
err := a.copyEmojiImages(emoji.Id, emojiImagePath, pathToDir)
|
||||
if err != nil {
|
||||
return model.NewAppError("BulkExport", "app.export.export_custom_emoji.copy_emoji_images.error", nil, "err="+err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
filePath := dirNameToExportEmoji + "/" + emoji.Id + "/image"
|
||||
|
||||
emojiImportObject := ImportLineFromEmoji(emoji, filePath)
|
||||
|
||||
if err := a.ExportWriteLine(writer, emojiImportObject); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Creates directory named 'exported_emoji' to copy the emoji files
|
||||
// Directory and the file specified by admin share the same path
|
||||
func (a *App) createDirForEmoji(file string, dirName string) string {
|
||||
pathToFile, _ := filepath.Abs(file)
|
||||
pathSlice := strings.Split(pathToFile, "/")
|
||||
if len(pathSlice) > 0 {
|
||||
pathSlice = pathSlice[:len(pathSlice)-1]
|
||||
}
|
||||
pathToDir := strings.Join(pathSlice, "/") + "/" + dirName
|
||||
|
||||
if _, err := os.Stat(pathToDir); os.IsNotExist(err) {
|
||||
os.Mkdir(pathToDir, os.ModePerm)
|
||||
}
|
||||
return pathToDir
|
||||
}
|
||||
|
||||
// Copies emoji files from 'data/emoji' dir to 'exported_emoji' dir
|
||||
func (a *App) copyEmojiImages(emojiId string, emojiImagePath string, pathToDir string) error {
|
||||
var err error
|
||||
|
||||
fromPath, err := os.Open(emojiImagePath)
|
||||
if fromPath == nil || err != nil {
|
||||
return errors.New("Error reading " + emojiImagePath + "file")
|
||||
}
|
||||
defer fromPath.Close()
|
||||
|
||||
emojiDir := pathToDir + "/" + emojiId
|
||||
|
||||
if _, err := os.Stat(emojiDir); os.IsNotExist(err) {
|
||||
os.Mkdir(emojiDir, os.ModePerm)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.New("Error creating directory for the emoji " + err.Error())
|
||||
}
|
||||
toPath, err := os.OpenFile(emojiDir+"/image", os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
return errors.New("Error creating the image file " + err.Error())
|
||||
}
|
||||
defer toPath.Close()
|
||||
|
||||
_, err = io.Copy(toPath, fromPath)
|
||||
if err != nil {
|
||||
return errors.New("Error copying emojis " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -139,3 +139,13 @@ func ImportReactionFromPost(reaction *model.Reaction) *ReactionImportData {
|
||||
CreateAt: &reaction.CreateAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ImportLineFromEmoji(emoji *model.Emoji, filePath string) *LineImportData {
|
||||
return &LineImportData{
|
||||
Type: "emoji",
|
||||
Emoji: &EmojiImportData{
|
||||
Name: &emoji.Name,
|
||||
Image: &filePath,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -103,3 +104,69 @@ func TestExportUserChannels(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirCreationForEmoji(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
pathToDir := th.App.createDirForEmoji("test.json", "exported_emoji_test")
|
||||
defer os.Remove(pathToDir)
|
||||
if _, err := os.Stat(pathToDir); os.IsNotExist(err) {
|
||||
t.Fatal("Directory exported_emoji_test should exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyEmojiImages(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
emoji := &model.Emoji{
|
||||
Id: th.BasicUser.Id,
|
||||
}
|
||||
|
||||
// Creating a dir named `exported_emoji_test` in the root of the repo
|
||||
pathToDir := "../exported_emoji_test"
|
||||
|
||||
os.Mkdir(pathToDir, 0777)
|
||||
defer os.RemoveAll(pathToDir)
|
||||
|
||||
filePath := "../data/emoji/" + emoji.Id
|
||||
emojiImagePath := filePath + "/image"
|
||||
|
||||
var _, err = os.Stat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
os.MkdirAll(filePath, 0777)
|
||||
}
|
||||
|
||||
// Creating a file with the name `image` to copy it to `exported_emoji_test`
|
||||
os.OpenFile(filePath+"/image", os.O_RDONLY|os.O_CREATE, 0777)
|
||||
defer os.RemoveAll(filePath)
|
||||
|
||||
copyError := th.App.copyEmojiImages(emoji.Id, emojiImagePath, pathToDir)
|
||||
if copyError != nil {
|
||||
t.Fatal(copyError)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(pathToDir + "/" + emoji.Id + "/image"); os.IsNotExist(err) {
|
||||
t.Fatal("File should exist ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportCustomEmoji(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
filePath := "../demo.json"
|
||||
|
||||
fileWriter, _ := os.Create(filePath)
|
||||
defer os.Remove(filePath)
|
||||
|
||||
pathToEmojiDir := "../data/emoji/"
|
||||
dirNameToExportEmoji := "exported_emoji_test"
|
||||
|
||||
err := th.App.ExportCustomEmoji(fileWriter, filePath, pathToEmojiDir, dirNameToExportEmoji)
|
||||
defer os.RemoveAll("../" + dirNameToExportEmoji)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
200
app/integration_action.go
Обычный файл
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
Обычный файл
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)
|
||||
}
|
||||
@@ -84,6 +84,20 @@ func (api *PluginAPI) SaveConfig(config *model.Config) *model.AppError {
|
||||
return api.app.SaveConfig(config, true)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetPluginConfig() map[string]interface{} {
|
||||
cfg := api.app.GetConfig()
|
||||
if pluginConfig, isOk := cfg.PluginSettings.Plugins[api.manifest.Id]; isOk {
|
||||
return pluginConfig
|
||||
}
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
|
||||
func (api *PluginAPI) SavePluginConfig(pluginConfig map[string]interface{}) *model.AppError {
|
||||
cfg := api.app.GetConfig()
|
||||
cfg.PluginSettings.Plugins[api.manifest.Id] = pluginConfig
|
||||
return api.app.SaveConfig(cfg, true)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetServerVersion() string {
|
||||
return model.CurrentVersion
|
||||
}
|
||||
@@ -132,8 +146,8 @@ func (api *PluginAPI) DeleteTeamMember(teamId, userId, requestorId string) *mode
|
||||
return api.app.RemoveUserFromTeam(teamId, userId, requestorId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetTeamMembers(teamId string, offset, limit int) ([]*model.TeamMember, *model.AppError) {
|
||||
return api.app.GetTeamMembers(teamId, offset, limit)
|
||||
func (api *PluginAPI) GetTeamMembers(teamId string, page, perPage int) ([]*model.TeamMember, *model.AppError) {
|
||||
return api.app.GetTeamMembers(teamId, page*perPage, perPage)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetTeamMember(teamId, userId string) (*model.TeamMember, *model.AppError) {
|
||||
@@ -246,8 +260,9 @@ func (api *PluginAPI) DeleteChannel(channelId string) *model.AppError {
|
||||
return api.app.DeleteChannel(channel, "")
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetPublicChannelsForTeam(teamId string, offset, limit int) (*model.ChannelList, *model.AppError) {
|
||||
return api.app.GetPublicChannelsForTeam(teamId, offset, limit)
|
||||
func (api *PluginAPI) GetPublicChannelsForTeam(teamId string, page, perPage int) ([]*model.Channel, *model.AppError) {
|
||||
channels, err := api.app.GetPublicChannelsForTeam(teamId, page*perPage, perPage)
|
||||
return *channels, err
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetChannel(channelId string) (*model.Channel, *model.AppError) {
|
||||
@@ -466,6 +481,24 @@ func (api *PluginAPI) GetTeamIcon(teamId string) ([]byte, *model.AppError) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (api *PluginAPI) SetTeamIcon(teamId string, data []byte) *model.AppError {
|
||||
team, err := api.app.GetTeam(teamId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileReader := bytes.NewReader(data)
|
||||
err = api.app.SetTeamIconFromFile(team, fileReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -63,6 +63,84 @@ func TestPluginAPIUpdateUserStatus(t *testing.T) {
|
||||
assert.Nil(t, status)
|
||||
}
|
||||
|
||||
func TestPluginAPISavePluginConfig(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
manifest := &model.Manifest{
|
||||
Id: "pluginid",
|
||||
SettingsSchema: &model.PluginSettingsSchema{
|
||||
Settings: []*model.PluginSetting{
|
||||
{Key: "MyStringSetting", Type: "text"},
|
||||
{Key: "MyIntSetting", Type: "text"},
|
||||
{Key: "MyBoolSetting", Type: "bool"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
api := NewPluginAPI(th.App, manifest)
|
||||
|
||||
pluginConfigJsonString := `{"mystringsetting": "str", "MyIntSetting": 32, "myboolsetting": true}`
|
||||
|
||||
var pluginConfig map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(pluginConfigJsonString), &pluginConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := api.SavePluginConfig(pluginConfig); err != nil{
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type Configuration struct {
|
||||
MyStringSetting string
|
||||
MyIntSetting int
|
||||
MyBoolSetting bool
|
||||
}
|
||||
|
||||
savedConfiguration := new(Configuration)
|
||||
if err := api.LoadPluginConfiguration(savedConfiguration); err != nil{
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedConfiguration := new(Configuration)
|
||||
if err := json.Unmarshal([]byte(pluginConfigJsonString), &expectedConfiguration); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedConfiguration, savedConfiguration)
|
||||
}
|
||||
|
||||
func TestPluginAPIGetPluginConfig(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
manifest := &model.Manifest{
|
||||
Id: "pluginid",
|
||||
SettingsSchema: &model.PluginSettingsSchema{
|
||||
Settings: []*model.PluginSetting{
|
||||
{Key: "MyStringSetting", Type: "text"},
|
||||
{Key: "MyIntSetting", Type: "text"},
|
||||
{Key: "MyBoolSetting", Type: "bool"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
api := NewPluginAPI(th.App, manifest)
|
||||
|
||||
pluginConfigJsonString := `{"mystringsetting": "str", "MyIntSetting": 32, "myboolsetting": true}`
|
||||
var pluginConfig map[string]interface{}
|
||||
|
||||
if err := json.Unmarshal([]byte(pluginConfigJsonString), &pluginConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.PluginSettings.Plugins["pluginid"] = pluginConfig
|
||||
})
|
||||
|
||||
savedPluginConfig := api.GetPluginConfig()
|
||||
assert.Equal(t, pluginConfig, savedPluginConfig)
|
||||
}
|
||||
|
||||
func TestPluginAPILoadPluginConfiguration(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
@@ -332,12 +410,42 @@ func TestPluginAPIGetTeamIcon(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
|
||||
// Get the team icon to check
|
||||
imageProfile, err := api.GetTeamIcon(th.BasicTeam.Id)
|
||||
teamIcon, err := api.GetTeamIcon(th.BasicTeam.Id)
|
||||
require.Nil(t, err)
|
||||
require.NotEmpty(t, imageProfile)
|
||||
require.NotEmpty(t, teamIcon)
|
||||
|
||||
colorful := color.NRGBA{255, 0, 0, 255}
|
||||
byteReader := bytes.NewReader(imageProfile)
|
||||
byteReader := bytes.NewReader(teamIcon)
|
||||
img2, _, err2 := image.Decode(byteReader)
|
||||
require.Nil(t, err2)
|
||||
require.Equal(t, img2.At(2, 3), colorful)
|
||||
}
|
||||
|
||||
func TestPluginAPISetTeamIcon(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
api := th.SetupPluginAPI()
|
||||
|
||||
// Create an 128 x 128 image
|
||||
img := image.NewRGBA(image.Rect(0, 0, 128, 128))
|
||||
// Draw a red dot at (2, 3)
|
||||
img.Set(2, 3, color.RGBA{255, 0, 0, 255})
|
||||
buf := new(bytes.Buffer)
|
||||
err := png.Encode(buf, img)
|
||||
require.Nil(t, err)
|
||||
dataBytes := buf.Bytes()
|
||||
|
||||
// Set the user profile image
|
||||
err = api.SetTeamIcon(th.BasicTeam.Id, dataBytes)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Get the user profile image to check
|
||||
teamIcon, err := api.GetTeamIcon(th.BasicTeam.Id)
|
||||
require.Nil(t, err)
|
||||
require.NotEmpty(t, teamIcon)
|
||||
|
||||
colorful := color.NRGBA{255, 0, 0, 255}
|
||||
byteReader := bytes.NewReader(teamIcon)
|
||||
img2, _, err2 := image.Decode(byteReader)
|
||||
require.Nil(t, err2)
|
||||
require.Equal(t, img2.At(2, 3), colorful)
|
||||
|
||||
109
app/post.go
109
app/post.go
@@ -7,17 +7,13 @@ import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
@@ -779,111 +775,6 @@ func (a *App) GetFileInfosForPost(postId string, readFromMaster bool) ([]*model.
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
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) PostWithProxyAddedToImageURLs(post *model.Post) *model.Post {
|
||||
if f := a.ImageProxyAdder(); f != nil {
|
||||
return post.WithRewrittenImageURLs(f)
|
||||
|
||||
242
app/post_test.go
242
app/post_test.go
@@ -6,8 +6,6 @@ package app
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -119,246 +117,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()
|
||||
|
||||
@@ -4,10 +4,14 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreateTeam(t *testing.T) {
|
||||
@@ -683,3 +687,45 @@ func TestAppUpdateTeamScheme(t *testing.T) {
|
||||
t.Fatal("Wrong Team SchemeId")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTeamMembers(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
var userIDs sort.StringSlice
|
||||
userIDs = append(userIDs, th.BasicUser.Id)
|
||||
userIDs = append(userIDs, th.BasicUser2.Id)
|
||||
|
||||
for i := 0; i < 8; i++ {
|
||||
user := model.User{
|
||||
Email: strings.ToLower(model.NewId()) + "success+test@example.com",
|
||||
Username: fmt.Sprintf("user%v", i),
|
||||
Password: "passwd1",
|
||||
}
|
||||
ruser, err := th.App.CreateUser(&user)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, ruser)
|
||||
defer th.App.PermanentDeleteUser(&user)
|
||||
|
||||
_, err = th.App.AddUserToTeam(th.BasicTeam.Id, ruser.Id, "")
|
||||
require.Nil(t, err)
|
||||
|
||||
// Store the user ids for comparison later
|
||||
userIDs = append(userIDs, ruser.Id)
|
||||
}
|
||||
// Sort them because the result of GetTeamMembers() is also sorted
|
||||
sort.Sort(userIDs)
|
||||
|
||||
// Fetch team members multipile times
|
||||
members, err := th.App.GetTeamMembers(th.BasicTeam.Id, 0, 5)
|
||||
require.Nil(t, err)
|
||||
// This should return 5 members
|
||||
members2, err := th.App.GetTeamMembers(th.BasicTeam.Id, 5, 6)
|
||||
require.Nil(t, err)
|
||||
members = append(members, members2...)
|
||||
|
||||
require.Equal(t, len(userIDs), len(members))
|
||||
for i, member := range members {
|
||||
assert.Equal(t, userIDs[i], member.UserId)
|
||||
}
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user