Merge branch 'master' into post-metadata
Этот коммит содержится в:
@@ -231,6 +231,7 @@ func Init(a *app.App, root *mux.Router) *API {
|
|||||||
api.InitScheme()
|
api.InitScheme()
|
||||||
api.InitImage()
|
api.InitImage()
|
||||||
api.InitTermsOfService()
|
api.InitTermsOfService()
|
||||||
|
api.InitAction()
|
||||||
|
|
||||||
root.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
|
root.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"github.com/mattermost/mattermost-server/model"
|
||||||
@@ -508,7 +509,9 @@ func TestExecuteGetCommand(t *testing.T) {
|
|||||||
|
|
||||||
commandResponse, resp := Client.ExecuteCommand(channel.Id, "/getcommand")
|
commandResponse, resp := Client.ExecuteCommand(channel.Id, "/getcommand")
|
||||||
CheckNoError(t, resp)
|
CheckNoError(t, resp)
|
||||||
|
assert.True(t, len(commandResponse.TriggerId) == 26)
|
||||||
|
|
||||||
|
expectedCommandResponse.TriggerId = commandResponse.TriggerId
|
||||||
expectedCommandResponse.Props["from_webhook"] = "true"
|
expectedCommandResponse.Props["from_webhook"] = "true"
|
||||||
require.Equal(t, expectedCommandResponse, commandResponse)
|
require.Equal(t, expectedCommandResponse, commandResponse)
|
||||||
}
|
}
|
||||||
@@ -566,7 +569,9 @@ func TestExecutePostCommand(t *testing.T) {
|
|||||||
|
|
||||||
commandResponse, resp := Client.ExecuteCommand(channel.Id, "/postcommand")
|
commandResponse, resp := Client.ExecuteCommand(channel.Id, "/postcommand")
|
||||||
CheckNoError(t, resp)
|
CheckNoError(t, resp)
|
||||||
|
assert.True(t, len(commandResponse.TriggerId) == 26)
|
||||||
|
|
||||||
|
expectedCommandResponse.TriggerId = commandResponse.TriggerId
|
||||||
expectedCommandResponse.Props["from_webhook"] = "true"
|
expectedCommandResponse.Props["from_webhook"] = "true"
|
||||||
require.Equal(t, expectedCommandResponse, commandResponse)
|
require.Equal(t, expectedCommandResponse, commandResponse)
|
||||||
|
|
||||||
|
|||||||
105
api4/integration_action.go
Обычный файл
105
api4/integration_action.go
Обычный файл
@@ -0,0 +1,105 @@
|
|||||||
|
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package api4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (api *API) InitAction() {
|
||||||
|
api.BaseRoutes.Post.Handle("/actions/{action_id:[A-Za-z0-9]+}", api.ApiSessionRequired(doPostAction)).Methods("POST")
|
||||||
|
|
||||||
|
api.BaseRoutes.ApiRoot.Handle("/actions/dialogs/open", api.ApiHandler(openDialog)).Methods("POST")
|
||||||
|
api.BaseRoutes.ApiRoot.Handle("/actions/dialogs/submit", api.ApiSessionRequired(submitDialog)).Methods("POST")
|
||||||
|
}
|
||||||
|
|
||||||
|
func doPostAction(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
c.RequirePostId().RequireActionId()
|
||||||
|
if c.Err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.App.SessionHasPermissionToChannelByPost(c.Session, c.Params.PostId, model.PERMISSION_READ_CHANNEL) {
|
||||||
|
c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionRequest := model.DoPostActionRequestFromJson(r.Body)
|
||||||
|
if actionRequest == nil {
|
||||||
|
actionRequest = &model.DoPostActionRequest{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err *model.AppError
|
||||||
|
resp := &model.PostActionAPIResponse{Status: "OK"}
|
||||||
|
|
||||||
|
if resp.TriggerId, err = c.App.DoPostAction(c.Params.PostId, c.Params.ActionId, c.Session.UserId, actionRequest.SelectedOption); err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := json.Marshal(resp)
|
||||||
|
|
||||||
|
w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func openDialog(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
var dialog model.OpenDialogRequest
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&dialog)
|
||||||
|
if err != nil {
|
||||||
|
c.SetInvalidParam("dialog")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if dialog.URL == "" {
|
||||||
|
c.SetInvalidParam("url")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.App.OpenInteractiveDialog(dialog); err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ReturnStatusOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func submitDialog(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
var submit model.SubmitDialogRequest
|
||||||
|
|
||||||
|
jsonErr := json.NewDecoder(r.Body).Decode(&submit)
|
||||||
|
if jsonErr != nil {
|
||||||
|
c.SetInvalidParam("dialog")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if submit.URL == "" {
|
||||||
|
c.SetInvalidParam("url")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submit.UserId = c.Session.UserId
|
||||||
|
|
||||||
|
if !c.App.SessionHasPermissionToChannel(c.Session, submit.ChannelId, model.PERMISSION_READ_CHANNEL) {
|
||||||
|
c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.App.SessionHasPermissionToTeam(c.Session, submit.TeamId, model.PERMISSION_VIEW_TEAM) {
|
||||||
|
c.SetPermissionError(model.PERMISSION_VIEW_TEAM)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.App.SubmitInteractiveDialog(submit)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := json.Marshal(resp)
|
||||||
|
|
||||||
|
w.Write(b)
|
||||||
|
}
|
||||||
148
api4/integration_action_test.go
Обычный файл
148
api4/integration_action_test.go
Обычный файл
@@ -0,0 +1,148 @@
|
|||||||
|
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package api4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/model"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOpenDialog(t *testing.T) {
|
||||||
|
th := Setup().InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
Client := th.Client
|
||||||
|
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1"
|
||||||
|
})
|
||||||
|
|
||||||
|
WebSocketClient, err := th.CreateWebSocketClient()
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
WebSocketClient.Listen()
|
||||||
|
|
||||||
|
_, triggerId, err := model.GenerateTriggerId(th.BasicUser.Id, th.App.AsymmetricSigningKey())
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
request := model.OpenDialogRequest{
|
||||||
|
TriggerId: triggerId,
|
||||||
|
URL: "http://localhost:8065",
|
||||||
|
Dialog: model.Dialog{
|
||||||
|
CallbackId: "callbackid",
|
||||||
|
Title: "Some Title",
|
||||||
|
Elements: []model.DialogElement{
|
||||||
|
model.DialogElement{
|
||||||
|
DisplayName: "Element Name",
|
||||||
|
Name: "element_name",
|
||||||
|
Type: "text",
|
||||||
|
Placeholder: "Enter a value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SubmitLabel: "Submit",
|
||||||
|
NotifyOnCancel: false,
|
||||||
|
State: "somestate",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pass, resp := Client.OpenInteractiveDialog(request)
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
assert.True(t, pass)
|
||||||
|
|
||||||
|
timeout := time.After(300 * time.Millisecond)
|
||||||
|
waiting := true
|
||||||
|
for waiting {
|
||||||
|
select {
|
||||||
|
case event := <-WebSocketClient.EventChannel:
|
||||||
|
if event.Event == model.WEBSOCKET_EVENT_OPEN_DIALOG {
|
||||||
|
waiting = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-timeout:
|
||||||
|
waiting = false
|
||||||
|
t.Fatal("should have received open_dialog event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should fail on bad trigger ID
|
||||||
|
request.TriggerId = "junk"
|
||||||
|
pass, resp = Client.OpenInteractiveDialog(request)
|
||||||
|
CheckBadRequestStatus(t, resp)
|
||||||
|
assert.False(t, pass)
|
||||||
|
|
||||||
|
// URL is required
|
||||||
|
request.TriggerId = triggerId
|
||||||
|
request.URL = ""
|
||||||
|
pass, resp = Client.OpenInteractiveDialog(request)
|
||||||
|
CheckBadRequestStatus(t, resp)
|
||||||
|
assert.False(t, pass)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubmitDialog(t *testing.T) {
|
||||||
|
th := Setup().InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
Client := th.Client
|
||||||
|
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1"
|
||||||
|
})
|
||||||
|
|
||||||
|
submit := model.SubmitDialogRequest{
|
||||||
|
CallbackId: "callbackid",
|
||||||
|
State: "somestate",
|
||||||
|
UserId: th.BasicUser.Id,
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
|
TeamId: th.BasicTeam.Id,
|
||||||
|
Submission: map[string]interface{}{"somename": "somevalue"},
|
||||||
|
}
|
||||||
|
|
||||||
|
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["somename"].(string)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "somevalue", val)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
submit.URL = ts.URL
|
||||||
|
|
||||||
|
submitResp, resp := Client.SubmitInteractiveDialog(submit)
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
assert.NotNil(t, submitResp)
|
||||||
|
|
||||||
|
submit.URL = ""
|
||||||
|
submitResp, resp = Client.SubmitInteractiveDialog(submit)
|
||||||
|
CheckBadRequestStatus(t, resp)
|
||||||
|
assert.Nil(t, submitResp)
|
||||||
|
|
||||||
|
submit.URL = ts.URL
|
||||||
|
submit.ChannelId = model.NewId()
|
||||||
|
submitResp, resp = Client.SubmitInteractiveDialog(submit)
|
||||||
|
CheckForbiddenStatus(t, resp)
|
||||||
|
assert.Nil(t, submitResp)
|
||||||
|
|
||||||
|
submit.URL = ts.URL
|
||||||
|
submit.ChannelId = th.BasicChannel.Id
|
||||||
|
submit.TeamId = model.NewId()
|
||||||
|
submitResp, resp = Client.SubmitInteractiveDialog(submit)
|
||||||
|
CheckForbiddenStatus(t, resp)
|
||||||
|
assert.Nil(t, submitResp)
|
||||||
|
}
|
||||||
25
api4/post.go
25
api4/post.go
@@ -25,7 +25,6 @@ func (api *API) InitPost() {
|
|||||||
api.BaseRoutes.Team.Handle("/posts/search", api.ApiSessionRequired(searchPosts)).Methods("POST")
|
api.BaseRoutes.Team.Handle("/posts/search", api.ApiSessionRequired(searchPosts)).Methods("POST")
|
||||||
api.BaseRoutes.Post.Handle("", api.ApiSessionRequired(updatePost)).Methods("PUT")
|
api.BaseRoutes.Post.Handle("", api.ApiSessionRequired(updatePost)).Methods("PUT")
|
||||||
api.BaseRoutes.Post.Handle("/patch", api.ApiSessionRequired(patchPost)).Methods("PUT")
|
api.BaseRoutes.Post.Handle("/patch", api.ApiSessionRequired(patchPost)).Methods("PUT")
|
||||||
api.BaseRoutes.Post.Handle("/actions/{action_id:[A-Za-z0-9]+}", api.ApiSessionRequired(doPostAction)).Methods("POST")
|
|
||||||
api.BaseRoutes.Post.Handle("/pin", api.ApiSessionRequired(pinPost)).Methods("POST")
|
api.BaseRoutes.Post.Handle("/pin", api.ApiSessionRequired(pinPost)).Methods("POST")
|
||||||
api.BaseRoutes.Post.Handle("/unpin", api.ApiSessionRequired(unpinPost)).Methods("POST")
|
api.BaseRoutes.Post.Handle("/unpin", api.ApiSessionRequired(unpinPost)).Methods("POST")
|
||||||
}
|
}
|
||||||
@@ -541,27 +540,3 @@ func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set(model.HEADER_ETAG_SERVER, model.GetEtagForFileInfos(infos))
|
w.Header().Set(model.HEADER_ETAG_SERVER, model.GetEtagForFileInfos(infos))
|
||||||
w.Write([]byte(model.FileInfosToJson(infos)))
|
w.Write([]byte(model.FileInfosToJson(infos)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func doPostAction(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
c.RequirePostId().RequireActionId()
|
|
||||||
if c.Err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.App.SessionHasPermissionToChannelByPost(c.Session, c.Params.PostId, model.PERMISSION_READ_CHANNEL) {
|
|
||||||
c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
actionRequest := model.DoPostActionRequestFromJson(r.Body)
|
|
||||||
if actionRequest == nil {
|
|
||||||
actionRequest = &model.DoPostActionRequest{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.App.DoPostAction(c.Params.PostId, c.Params.ActionId, c.Session.UserId, actionRequest.SelectedOption); err != nil {
|
|
||||||
c.Err = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ReturnStatusOK(w)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -747,3 +748,46 @@ func TestGetChannelMembersTimezones(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.Equal(t, 2, len(timezones))
|
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:], " ")
|
message := strings.Join(parts[1:], " ")
|
||||||
provider := GetCommandProvider(trigger)
|
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 provider != nil {
|
||||||
if cmd := provider.GetCommand(a, args.T); cmd != nil {
|
if cmd := provider.GetCommand(a, args.T); cmd != nil {
|
||||||
response := provider.DoCommand(a, args, message)
|
response := provider.DoCommand(a, args, message)
|
||||||
@@ -174,6 +181,7 @@ func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *
|
|||||||
return nil, appErr
|
return nil, appErr
|
||||||
}
|
}
|
||||||
if cmd != nil {
|
if cmd != nil {
|
||||||
|
response.TriggerId = clientTriggerId
|
||||||
return a.HandleCommandResponse(cmd, args, response, true)
|
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("command", "/"+trigger)
|
||||||
p.Set("text", message)
|
p.Set("text", message)
|
||||||
|
|
||||||
|
p.Set("trigger_id", triggerId)
|
||||||
|
|
||||||
hook, appErr := a.CreateCommandWebhook(cmd.Id, args)
|
hook, appErr := a.CreateCommandWebhook(cmd.Id, args)
|
||||||
if appErr != nil {
|
if appErr != nil {
|
||||||
return nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, appErr.Error(), http.StatusInternalServerError)
|
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 {
|
if response == nil {
|
||||||
return nil, model.NewAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusInternalServerError)
|
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)
|
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) {
|
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 := &model.Post{}
|
||||||
post.ChannelId = args.ChannelId
|
post.ChannelId = args.ChannelId
|
||||||
post.RootId = args.RootId
|
post.RootId = args.RootId
|
||||||
|
|||||||
@@ -5,14 +5,17 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"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 {
|
if err := a.ExportVersion(writer); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -32,6 +35,9 @@ func (a *App) BulkExport(writer io.Writer) *model.AppError {
|
|||||||
if err := a.ExportAllPosts(writer); err != nil {
|
if err := a.ExportAllPosts(writer); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := a.ExportCustomEmoji(writer, file, pathToEmojiDir, dirNameToExportEmoji); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -338,3 +344,88 @@ func (a *App) BuildPostReactions(postId string) (*[]ReactionImportData, *model.A
|
|||||||
return &reactionsOfPost, nil
|
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,
|
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
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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)
|
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 {
|
func (api *PluginAPI) GetServerVersion() string {
|
||||||
return model.CurrentVersion
|
return model.CurrentVersion
|
||||||
}
|
}
|
||||||
@@ -132,8 +146,8 @@ func (api *PluginAPI) DeleteTeamMember(teamId, userId, requestorId string) *mode
|
|||||||
return api.app.RemoveUserFromTeam(teamId, userId, requestorId)
|
return api.app.RemoveUserFromTeam(teamId, userId, requestorId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *PluginAPI) GetTeamMembers(teamId string, offset, limit int) ([]*model.TeamMember, *model.AppError) {
|
func (api *PluginAPI) GetTeamMembers(teamId string, page, perPage int) ([]*model.TeamMember, *model.AppError) {
|
||||||
return api.app.GetTeamMembers(teamId, offset, limit)
|
return api.app.GetTeamMembers(teamId, page*perPage, perPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *PluginAPI) GetTeamMember(teamId, userId string) (*model.TeamMember, *model.AppError) {
|
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, "")
|
return api.app.DeleteChannel(channel, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *PluginAPI) GetPublicChannelsForTeam(teamId string, offset, limit int) (*model.ChannelList, *model.AppError) {
|
func (api *PluginAPI) GetPublicChannelsForTeam(teamId string, page, perPage int) ([]*model.Channel, *model.AppError) {
|
||||||
return api.app.GetPublicChannelsForTeam(teamId, offset, limit)
|
channels, err := api.app.GetPublicChannelsForTeam(teamId, page*perPage, perPage)
|
||||||
|
return *channels, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *PluginAPI) GetChannel(channelId string) (*model.Channel, *model.AppError) {
|
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
|
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
|
// Plugin Section
|
||||||
|
|
||||||
func (api *PluginAPI) GetPlugins() ([]*model.Manifest, *model.AppError) {
|
func (api *PluginAPI) GetPlugins() ([]*model.Manifest, *model.AppError) {
|
||||||
|
|||||||
@@ -63,6 +63,84 @@ func TestPluginAPIUpdateUserStatus(t *testing.T) {
|
|||||||
assert.Nil(t, status)
|
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) {
|
func TestPluginAPILoadPluginConfiguration(t *testing.T) {
|
||||||
th := Setup().InitBasic()
|
th := Setup().InitBasic()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
@@ -332,12 +410,42 @@ func TestPluginAPIGetTeamIcon(t *testing.T) {
|
|||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
// Get the team icon to check
|
// 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.Nil(t, err)
|
||||||
require.NotEmpty(t, imageProfile)
|
require.NotEmpty(t, teamIcon)
|
||||||
|
|
||||||
colorful := color.NRGBA{255, 0, 0, 255}
|
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)
|
img2, _, err2 := image.Decode(byteReader)
|
||||||
require.Nil(t, err2)
|
require.Nil(t, err2)
|
||||||
require.Equal(t, img2.At(2, 3), colorful)
|
require.Equal(t, img2.At(2, 3), colorful)
|
||||||
|
|||||||
109
app/post.go
109
app/post.go
@@ -7,17 +7,13 @@ import (
|
|||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/mlog"
|
"github.com/mattermost/mattermost-server/mlog"
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"github.com/mattermost/mattermost-server/model"
|
||||||
"github.com/mattermost/mattermost-server/plugin"
|
"github.com/mattermost/mattermost-server/plugin"
|
||||||
"github.com/mattermost/mattermost-server/services/httpservice"
|
|
||||||
"github.com/mattermost/mattermost-server/store"
|
"github.com/mattermost/mattermost-server/store"
|
||||||
"github.com/mattermost/mattermost-server/utils"
|
"github.com/mattermost/mattermost-server/utils"
|
||||||
)
|
)
|
||||||
@@ -779,111 +775,6 @@ func (a *App) GetFileInfosForPost(postId string, readFromMaster bool) ([]*model.
|
|||||||
return infos, nil
|
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 {
|
func (a *App) PostWithProxyAddedToImageURLs(post *model.Post) *model.Post {
|
||||||
if f := a.ImageProxyAdder(); f != nil {
|
if f := a.ImageProxyAdder(); f != nil {
|
||||||
return post.WithRewrittenImageURLs(f)
|
return post.WithRewrittenImageURLs(f)
|
||||||
|
|||||||
242
app/post_test.go
242
app/post_test.go
@@ -6,8 +6,6 @@ package app
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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) {
|
func TestPostChannelMentions(t *testing.T) {
|
||||||
th := Setup().InitBasic()
|
th := Setup().InitBasic()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|||||||
@@ -4,10 +4,14 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"github.com/mattermost/mattermost-server/model"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCreateTeam(t *testing.T) {
|
func TestCreateTeam(t *testing.T) {
|
||||||
@@ -683,3 +687,45 @@ func TestAppUpdateTeamScheme(t *testing.T) {
|
|||||||
t.Fatal("Wrong Team SchemeId")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -180,7 +180,14 @@ func bulkExportCmdF(command *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
defer fileWriter.Close()
|
defer fileWriter.Close()
|
||||||
|
|
||||||
if err := a.BulkExport(fileWriter); err != nil {
|
// Path to directory of custom emoji
|
||||||
|
pathToEmojiDir := "data/emoji/"
|
||||||
|
|
||||||
|
// Name of the directory to export custom emoji
|
||||||
|
dirNameToExportEmoji := "exported_emoji"
|
||||||
|
|
||||||
|
// args[0] points to the filename/filepath passed with export bulk command
|
||||||
|
if err := a.BulkExport(fileWriter, args[0], pathToEmojiDir, dirNameToExportEmoji); err != nil {
|
||||||
CommandPrettyPrintln(err.Error())
|
CommandPrettyPrintln(err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
28
i18n/en.json
28
i18n/en.json
@@ -7,6 +7,34 @@
|
|||||||
"id": "api.admin.add_certificate.array.app_error",
|
"id": "api.admin.add_certificate.array.app_error",
|
||||||
"translation": "No file under 'certificate' in request."
|
"translation": "No file under 'certificate' in request."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "app.submit_interactive_dialog.json_error",
|
||||||
|
"translation": "Encountered an error encoding JSON for the interactive dialog."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "interactive_message.generate_trigger_id.signing_failed",
|
||||||
|
"translation": "Failed to sign generatedd trigger ID for interactive dialog."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "interactive_message.decode_trigger_id.base64_decode_failed",
|
||||||
|
"translation": "Failed to decode base64 for trigger ID for interactive dialog."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "interactive_message.decode_trigger_id.missing_data",
|
||||||
|
"translation": "Trigger ID missing required data for interactive dialog."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "interactive_message.decode_trigger_id.expired",
|
||||||
|
"translation": "Trigger ID for interactive dialog is expired. Trigger IDs live for a maximum of {{.Seconds}} seconds."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "interactive_message.decode_trigger_id.signature_decode_failed",
|
||||||
|
"translation": "Failed to decode base64 signature of trigger ID for interactive dialog."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "interactive_message.decode_trigger_id.verify_signature_failed",
|
||||||
|
"translation": "Signature verification failed of trigger ID for interactive dialog."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.admin.add_certificate.no_file.app_error",
|
"id": "api.admin.add_certificate.no_file.app_error",
|
||||||
"translation": "No file under 'certificate' in request."
|
"translation": "No file under 'certificate' in request."
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -2224,7 +2225,7 @@ func (c *Client4) SearchPostsWithParams(teamId string, params *SearchParameter)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchPosts returns any posts with matching terms string, including .
|
// SearchPosts returns any posts with matching terms string, including.
|
||||||
func (c *Client4) SearchPostsWithMatches(teamId string, terms string, isOrSearch bool) (*PostSearchResults, *Response) {
|
func (c *Client4) SearchPostsWithMatches(teamId string, terms string, isOrSearch bool) (*PostSearchResults, *Response) {
|
||||||
requestBody := map[string]interface{}{"terms": terms, "is_or_search": isOrSearch}
|
requestBody := map[string]interface{}{"terms": terms, "is_or_search": isOrSearch}
|
||||||
if r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/posts/search", StringInterfaceToJson(requestBody)); err != nil {
|
if r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/posts/search", StringInterfaceToJson(requestBody)); err != nil {
|
||||||
@@ -2245,6 +2246,34 @@ func (c *Client4) DoPostAction(postId, actionId string) (bool, *Response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenInteractiveDialog sends a WebSocket event to a user's clients to
|
||||||
|
// open interactive dialogs, based on the provided trigger ID and other
|
||||||
|
// provided data. Used with interactive message buttons, menus and
|
||||||
|
// slash commands.
|
||||||
|
func (c *Client4) OpenInteractiveDialog(request OpenDialogRequest) (bool, *Response) {
|
||||||
|
b, _ := json.Marshal(request)
|
||||||
|
if r, err := c.DoApiPost("/actions/dialogs/open", string(b)); err != nil {
|
||||||
|
return false, BuildErrorResponse(r, err)
|
||||||
|
} else {
|
||||||
|
defer closeBody(r)
|
||||||
|
return CheckStatusOK(r), BuildResponse(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitInteractiveDialog will submit the provided dialog data to the integration
|
||||||
|
// configured by the URL. Used with the interactive dialogs integration feature.
|
||||||
|
func (c *Client4) SubmitInteractiveDialog(request SubmitDialogRequest) (*SubmitDialogResponse, *Response) {
|
||||||
|
b, _ := json.Marshal(request)
|
||||||
|
if r, err := c.DoApiPost("/actions/dialogs/submit", string(b)); err != nil {
|
||||||
|
return nil, BuildErrorResponse(r, err)
|
||||||
|
} else {
|
||||||
|
defer closeBody(r)
|
||||||
|
var resp SubmitDialogResponse
|
||||||
|
json.NewDecoder(r.Body).Decode(&resp)
|
||||||
|
return &resp, BuildResponse(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// File Section
|
// File Section
|
||||||
|
|
||||||
// UploadFile will upload a file to a channel using a multipart request, to be later attached to a post.
|
// UploadFile will upload a file to a channel using a multipart request, to be later attached to a post.
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type CommandArgs struct {
|
|||||||
TeamId string `json:"team_id"`
|
TeamId string `json:"team_id"`
|
||||||
RootId string `json:"root_id"`
|
RootId string `json:"root_id"`
|
||||||
ParentId string `json:"parent_id"`
|
ParentId string `json:"parent_id"`
|
||||||
|
TriggerId string `json:"trigger_id,omitempty"`
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
SiteURL string `json:"-"`
|
SiteURL string `json:"-"`
|
||||||
T goi18n.TranslateFunc `json:"-"`
|
T goi18n.TranslateFunc `json:"-"`
|
||||||
|
|||||||
@@ -18,14 +18,16 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CommandResponse struct {
|
type CommandResponse struct {
|
||||||
ResponseType string `json:"response_type"`
|
ResponseType string `json:"response_type"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
IconURL string `json:"icon_url"`
|
IconURL string `json:"icon_url"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Props StringInterface `json:"props"`
|
Props StringInterface `json:"props"`
|
||||||
GotoLocation string `json:"goto_location"`
|
GotoLocation string `json:"goto_location"`
|
||||||
Attachments []*SlackAttachment `json:"attachments"`
|
TriggerId string `json:"trigger_id"`
|
||||||
|
Attachments []*SlackAttachment `json:"attachments"`
|
||||||
|
ExtraResponses []*CommandResponse `json:"extra_responses"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *CommandResponse) ToJson() string {
|
func (o *CommandResponse) ToJson() string {
|
||||||
@@ -63,5 +65,11 @@ func CommandResponseFromJson(data io.Reader) (*CommandResponse, error) {
|
|||||||
|
|
||||||
o.Attachments = StringifySlackFieldValue(o.Attachments)
|
o.Attachments = StringifySlackFieldValue(o.Attachments)
|
||||||
|
|
||||||
|
if o.ExtraResponses != nil {
|
||||||
|
for _, resp := range o.ExtraResponses {
|
||||||
|
resp.Attachments = StringifySlackFieldValue(resp.Attachments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &o, nil
|
return &o, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,71 @@ func TestCommandResponseFromJson(t *testing.T) {
|
|||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"multiple responses returned",
|
||||||
|
`
|
||||||
|
{
|
||||||
|
"text": "message 1",
|
||||||
|
"extra_responses": [
|
||||||
|
{"text": "message 2"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
&CommandResponse{
|
||||||
|
Text: "message 1",
|
||||||
|
ExtraResponses: []*CommandResponse{
|
||||||
|
&CommandResponse{
|
||||||
|
Text: "message 2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"multiple responses returned, with attachments",
|
||||||
|
`
|
||||||
|
{
|
||||||
|
"text": "message 1",
|
||||||
|
"attachments":[{"fields":[{"title":"foo","value":"bar","short":true}]}],
|
||||||
|
"extra_responses": [
|
||||||
|
{
|
||||||
|
"text": "message 2",
|
||||||
|
"attachments":[{"fields":[{"title":"foo 2","value":"bar 2","short":false}]}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
&CommandResponse{
|
||||||
|
Text: "message 1",
|
||||||
|
Attachments: []*SlackAttachment{
|
||||||
|
{
|
||||||
|
Fields: []*SlackAttachmentField{
|
||||||
|
{
|
||||||
|
Title: "foo",
|
||||||
|
Value: "bar",
|
||||||
|
Short: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExtraResponses: []*CommandResponse{
|
||||||
|
&CommandResponse{
|
||||||
|
Text: "message 2",
|
||||||
|
Attachments: []*SlackAttachment{
|
||||||
|
{
|
||||||
|
Fields: []*SlackAttachmentField{
|
||||||
|
{
|
||||||
|
Title: "foo 2",
|
||||||
|
Value: "bar 2",
|
||||||
|
Short: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
|
|||||||
266
model/integration_action.go
Обычный файл
266
model/integration_action.go
Обычный файл
@@ -0,0 +1,266 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/asn1"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
POST_ACTION_TYPE_BUTTON = "button"
|
||||||
|
POST_ACTION_TYPE_SELECT = "select"
|
||||||
|
INTERACTIVE_DIALOG_TRIGGER_TIMEOUT_MILLISECONDS = 3000
|
||||||
|
)
|
||||||
|
|
||||||
|
type DoPostActionRequest struct {
|
||||||
|
SelectedOption string `json:"selected_option"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostAction struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
DataSource string `json:"data_source"`
|
||||||
|
Options []*PostActionOptions `json:"options"`
|
||||||
|
Integration *PostActionIntegration `json:"integration,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostActionOptions struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostActionIntegration struct {
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Context map[string]interface{} `json:"context,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostActionIntegrationRequest struct {
|
||||||
|
UserId string `json:"user_id"`
|
||||||
|
ChannelId string `json:"channel_id"`
|
||||||
|
TeamId string `json:"team_id"`
|
||||||
|
PostId string `json:"post_id"`
|
||||||
|
TriggerId string `json:"trigger_id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
DataSource string `json:"data_source"`
|
||||||
|
Context map[string]interface{} `json:"context,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostActionIntegrationResponse struct {
|
||||||
|
Update *Post `json:"update"`
|
||||||
|
EphemeralText string `json:"ephemeral_text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostActionAPIResponse struct {
|
||||||
|
Status string `json:"status"` // needed to maintain backwards compatibility
|
||||||
|
TriggerId string `json:"trigger_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Dialog struct {
|
||||||
|
CallbackId string `json:"callback_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
IconURL string `json:"icon_url"`
|
||||||
|
Elements []DialogElement `json:"elements"`
|
||||||
|
SubmitLabel string `json:"submit_label"`
|
||||||
|
NotifyOnCancel bool `json:"notify_on_cancel"`
|
||||||
|
State string `json:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DialogElement struct {
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
SubType string `json:"subtype"`
|
||||||
|
Default string `json:"default"`
|
||||||
|
Placeholder string `json:"placeholder"`
|
||||||
|
HelpText string `json:"help_text"`
|
||||||
|
Optional bool `json:"optional"`
|
||||||
|
MinLength int `json:"min_length"`
|
||||||
|
MaxLength int `json:"max_length"`
|
||||||
|
DataSource string `json:"data_source"`
|
||||||
|
Options []*PostActionOptions `json:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenDialogRequest struct {
|
||||||
|
TriggerId string `json:"trigger_id"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Dialog Dialog `json:"dialog"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubmitDialogRequest struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
CallbackId string `json:"callback_id"`
|
||||||
|
State string `json:"state"`
|
||||||
|
UserId string `json:"user_id"`
|
||||||
|
ChannelId string `json:"channel_id"`
|
||||||
|
TeamId string `json:"team_id"`
|
||||||
|
Submission map[string]interface{} `json:"submission"`
|
||||||
|
Cancelled bool `json:"cancelled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubmitDialogResponse struct {
|
||||||
|
Errors map[string]string `json:"errors,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PostActionIntegrationRequest) ToJson() []byte {
|
||||||
|
b, _ := json.Marshal(r)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateTriggerId(userId string, s crypto.Signer) (string, string, *AppError) {
|
||||||
|
clientTriggerId := NewId()
|
||||||
|
triggerData := strings.Join([]string{clientTriggerId, userId, strconv.FormatInt(GetMillis(), 10)}, ":") + ":"
|
||||||
|
|
||||||
|
h := crypto.SHA256
|
||||||
|
sum := h.New()
|
||||||
|
sum.Write([]byte(triggerData))
|
||||||
|
signature, err := s.Sign(rand.Reader, sum.Sum(nil), h)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", NewAppError("GenerateTriggerId", "interactive_message.generate_trigger_id.signing_failed", nil, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
base64Sig := base64.StdEncoding.EncodeToString(signature)
|
||||||
|
|
||||||
|
triggerId := base64.StdEncoding.EncodeToString([]byte(triggerData + base64Sig))
|
||||||
|
return clientTriggerId, triggerId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PostActionIntegrationRequest) GenerateTriggerId(s crypto.Signer) (string, string, *AppError) {
|
||||||
|
clientTriggerId, triggerId, err := GenerateTriggerId(r.UserId, s)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.TriggerId = triggerId
|
||||||
|
return clientTriggerId, triggerId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeAndVerifyTriggerId(triggerId string, s *ecdsa.PrivateKey) (string, string, *AppError) {
|
||||||
|
triggerIdBytes, err := base64.StdEncoding.DecodeString(triggerId)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.base64_decode_failed", nil, err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
split := strings.Split(string(triggerIdBytes), ":")
|
||||||
|
if len(split) != 4 {
|
||||||
|
return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.missing_data", nil, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientTriggerId := split[0]
|
||||||
|
userId := split[1]
|
||||||
|
timestampStr := split[2]
|
||||||
|
timestamp, _ := strconv.ParseInt(timestampStr, 10, 64)
|
||||||
|
|
||||||
|
now := GetMillis()
|
||||||
|
if now-timestamp > INTERACTIVE_DIALOG_TRIGGER_TIMEOUT_MILLISECONDS {
|
||||||
|
return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.expired", map[string]interface{}{"Seconds": INTERACTIVE_DIALOG_TRIGGER_TIMEOUT_MILLISECONDS / 1000}, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
signature, err := base64.StdEncoding.DecodeString(split[3])
|
||||||
|
if err != nil {
|
||||||
|
return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.base64_decode_failed_signature", nil, err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
var esig struct {
|
||||||
|
R, S *big.Int
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := asn1.Unmarshal([]byte(signature), &esig); err != nil {
|
||||||
|
return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.signature_decode_failed", nil, err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerData := strings.Join([]string{clientTriggerId, userId, timestampStr}, ":") + ":"
|
||||||
|
|
||||||
|
h := crypto.SHA256
|
||||||
|
sum := h.New()
|
||||||
|
sum.Write([]byte(triggerData))
|
||||||
|
|
||||||
|
if !ecdsa.Verify(&s.PublicKey, sum.Sum(nil), esig.R, esig.S) {
|
||||||
|
return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.verify_signature_failed", nil, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientTriggerId, userId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OpenDialogRequest) DecodeAndVerifyTriggerId(s *ecdsa.PrivateKey) (string, string, *AppError) {
|
||||||
|
return DecodeAndVerifyTriggerId(r.TriggerId, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PostActionIntegrationRequestFromJson(data io.Reader) *PostActionIntegrationRequest {
|
||||||
|
var o *PostActionIntegrationRequest
|
||||||
|
err := json.NewDecoder(data).Decode(&o)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PostActionIntegrationResponse) ToJson() []byte {
|
||||||
|
b, _ := json.Marshal(r)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func PostActionIntegrationResponseFromJson(data io.Reader) *PostActionIntegrationResponse {
|
||||||
|
var o *PostActionIntegrationResponse
|
||||||
|
err := json.NewDecoder(data).Decode(&o)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Post) StripActionIntegrations() {
|
||||||
|
attachments := o.Attachments()
|
||||||
|
if o.Props["attachments"] != nil {
|
||||||
|
o.Props["attachments"] = attachments
|
||||||
|
}
|
||||||
|
for _, attachment := range attachments {
|
||||||
|
for _, action := range attachment.Actions {
|
||||||
|
action.Integration = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Post) GetAction(id string) *PostAction {
|
||||||
|
for _, attachment := range o.Attachments() {
|
||||||
|
for _, action := range attachment.Actions {
|
||||||
|
if action.Id == id {
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Post) GenerateActionIds() {
|
||||||
|
if o.Props["attachments"] != nil {
|
||||||
|
o.Props["attachments"] = o.Attachments()
|
||||||
|
}
|
||||||
|
if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok {
|
||||||
|
for _, attachment := range attachments {
|
||||||
|
for _, action := range attachment.Actions {
|
||||||
|
if action.Id == "" {
|
||||||
|
action.Id = NewId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DoPostActionRequestFromJson(data io.Reader) *DoPostActionRequest {
|
||||||
|
var o *DoPostActionRequest
|
||||||
|
json.NewDecoder(data).Decode(&o)
|
||||||
|
return o
|
||||||
|
}
|
||||||
111
model/integration_action_test.go
Обычный файл
111
model/integration_action_test.go
Обычный файл
@@ -0,0 +1,111 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTriggerIdDecodeAndVerification(t *testing.T) {
|
||||||
|
|
||||||
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
t.Run("should succeed decoding and validation", func(t *testing.T) {
|
||||||
|
userId := NewId()
|
||||||
|
clientTriggerId, triggerId, err := GenerateTriggerId(userId, key)
|
||||||
|
decodedClientTriggerId, decodedUserId, err := DecodeAndVerifyTriggerId(triggerId, key)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, clientTriggerId, decodedClientTriggerId)
|
||||||
|
assert.Equal(t, userId, decodedUserId)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should succeed decoding and validation through request structs", func(t *testing.T) {
|
||||||
|
actionReq := &PostActionIntegrationRequest{
|
||||||
|
UserId: NewId(),
|
||||||
|
}
|
||||||
|
clientTriggerId, triggerId, err := actionReq.GenerateTriggerId(key)
|
||||||
|
dialogReq := &OpenDialogRequest{TriggerId: triggerId}
|
||||||
|
decodedClientTriggerId, decodedUserId, err := dialogReq.DecodeAndVerifyTriggerId(key)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, clientTriggerId, decodedClientTriggerId)
|
||||||
|
assert.Equal(t, actionReq.UserId, decodedUserId)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should fail on base64 decode", func(t *testing.T) {
|
||||||
|
_, _, err := DecodeAndVerifyTriggerId("junk!", key)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
assert.Equal(t, "interactive_message.decode_trigger_id.base64_decode_failed", err.Id)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should fail on trigger parsing", func(t *testing.T) {
|
||||||
|
_, _, err := DecodeAndVerifyTriggerId(base64.StdEncoding.EncodeToString([]byte("junk!")), key)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
assert.Equal(t, "interactive_message.decode_trigger_id.missing_data", err.Id)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should fail on expired timestamp", func(t *testing.T) {
|
||||||
|
_, _, err := DecodeAndVerifyTriggerId(base64.StdEncoding.EncodeToString([]byte("some-trigger-id:some-user-id:1234567890:junksignature")), key)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
assert.Equal(t, "interactive_message.decode_trigger_id.expired", err.Id)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should fail on base64 decoding signature", func(t *testing.T) {
|
||||||
|
_, _, err := DecodeAndVerifyTriggerId(base64.StdEncoding.EncodeToString([]byte("some-trigger-id:some-user-id:12345678900000:junk!")), key)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
assert.Equal(t, "interactive_message.decode_trigger_id.base64_decode_failed_signature", err.Id)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should fail on bad signature", func(t *testing.T) {
|
||||||
|
_, _, err := DecodeAndVerifyTriggerId(base64.StdEncoding.EncodeToString([]byte("some-trigger-id:some-user-id:12345678900000:junk")), key)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
assert.Equal(t, "interactive_message.decode_trigger_id.signature_decode_failed", err.Id)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should fail on bad key", func(t *testing.T) {
|
||||||
|
_, triggerId, err := GenerateTriggerId(NewId(), key)
|
||||||
|
newKey, keyErr := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
require.Nil(t, keyErr)
|
||||||
|
_, _, err = DecodeAndVerifyTriggerId(triggerId, newKey)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
assert.Equal(t, "interactive_message.decode_trigger_id.verify_signature_failed", err.Id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostActionIntegrationRequestToJson(t *testing.T) {
|
||||||
|
o := PostActionIntegrationRequest{UserId: NewId(), Context: StringInterface{"a": "abc"}}
|
||||||
|
j := o.ToJson()
|
||||||
|
ro := PostActionIntegrationRequestFromJson(bytes.NewReader(j))
|
||||||
|
|
||||||
|
assert.NotNil(t, ro)
|
||||||
|
assert.Equal(t, o, *ro)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostActionIntegrationRequestFromJsonError(t *testing.T) {
|
||||||
|
ro := PostActionIntegrationRequestFromJson(strings.NewReader(""))
|
||||||
|
assert.Nil(t, ro)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostActionIntegrationResponseToJson(t *testing.T) {
|
||||||
|
o := PostActionIntegrationResponse{Update: &Post{Id: NewId(), Message: NewId()}, EphemeralText: NewId()}
|
||||||
|
j := o.ToJson()
|
||||||
|
ro := PostActionIntegrationResponseFromJson(bytes.NewReader(j))
|
||||||
|
|
||||||
|
assert.NotNil(t, ro)
|
||||||
|
assert.Equal(t, o, *ro)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostActionIntegrationResponseFromJsonError(t *testing.T) {
|
||||||
|
ro := PostActionIntegrationResponseFromJson(strings.NewReader(""))
|
||||||
|
assert.Nil(t, ro)
|
||||||
|
}
|
||||||
112
model/post.go
112
model/post.go
@@ -50,8 +50,6 @@ const (
|
|||||||
PROPS_ADD_CHANNEL_MEMBER = "add_channel_member"
|
PROPS_ADD_CHANNEL_MEMBER = "add_channel_member"
|
||||||
POST_PROPS_ADDED_USER_ID = "addedUserId"
|
POST_PROPS_ADDED_USER_ID = "addedUserId"
|
||||||
POST_PROPS_DELETE_BY = "deleteBy"
|
POST_PROPS_DELETE_BY = "deleteBy"
|
||||||
POST_ACTION_TYPE_BUTTON = "button"
|
|
||||||
POST_ACTION_TYPE_SELECT = "select"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Post struct {
|
type Post struct {
|
||||||
@@ -135,44 +133,6 @@ type PostForIndexing struct {
|
|||||||
ParentCreateAt *int64 `json:"parent_create_at"`
|
ParentCreateAt *int64 `json:"parent_create_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DoPostActionRequest struct {
|
|
||||||
SelectedOption string `json:"selected_option"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostAction struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
DataSource string `json:"data_source"`
|
|
||||||
Options []*PostActionOptions `json:"options"`
|
|
||||||
Integration *PostActionIntegration `json:"integration,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostActionOptions struct {
|
|
||||||
Text string `json:"text"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostActionIntegration struct {
|
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
Context StringInterface `json:"context,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostActionIntegrationRequest struct {
|
|
||||||
UserId string `json:"user_id"`
|
|
||||||
ChannelId string `json:"channel_id"`
|
|
||||||
TeamId string `json:"team_id"`
|
|
||||||
PostId string `json:"post_id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
DataSource string `json:"data_source"`
|
|
||||||
Context StringInterface `json:"context,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostActionIntegrationResponse struct {
|
|
||||||
Update *Post `json:"update"`
|
|
||||||
EphemeralText string `json:"ephemeral_text"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shallowly clone the a post
|
// Shallowly clone the a post
|
||||||
func (o *Post) Clone() *Post {
|
func (o *Post) Clone() *Post {
|
||||||
copy := *o
|
copy := *o
|
||||||
@@ -416,34 +376,6 @@ func (o *Post) ChannelMentions() []string {
|
|||||||
return ChannelMentions(o.Message)
|
return ChannelMentions(o.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *PostActionIntegrationRequest) ToJson() string {
|
|
||||||
b, _ := json.Marshal(r)
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PostActionIntegrationRequesteFromJson(data io.Reader) *PostActionIntegrationRequest {
|
|
||||||
var o *PostActionIntegrationRequest
|
|
||||||
err := json.NewDecoder(data).Decode(&o)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PostActionIntegrationResponse) ToJson() string {
|
|
||||||
b, _ := json.Marshal(r)
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PostActionIntegrationResponseFromJson(data io.Reader) *PostActionIntegrationResponse {
|
|
||||||
var o *PostActionIntegrationResponse
|
|
||||||
err := json.NewDecoder(data).Decode(&o)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Post) Attachments() []*SlackAttachment {
|
func (o *Post) Attachments() []*SlackAttachment {
|
||||||
if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok {
|
if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok {
|
||||||
return attachments
|
return attachments
|
||||||
@@ -462,44 +394,6 @@ func (o *Post) Attachments() []*SlackAttachment {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Post) StripActionIntegrations() {
|
|
||||||
attachments := o.Attachments()
|
|
||||||
if o.Props["attachments"] != nil {
|
|
||||||
o.Props["attachments"] = attachments
|
|
||||||
}
|
|
||||||
for _, attachment := range attachments {
|
|
||||||
for _, action := range attachment.Actions {
|
|
||||||
action.Integration = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Post) GetAction(id string) *PostAction {
|
|
||||||
for _, attachment := range o.Attachments() {
|
|
||||||
for _, action := range attachment.Actions {
|
|
||||||
if action.Id == id {
|
|
||||||
return action
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Post) GenerateActionIds() {
|
|
||||||
if o.Props["attachments"] != nil {
|
|
||||||
o.Props["attachments"] = o.Attachments()
|
|
||||||
}
|
|
||||||
if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok {
|
|
||||||
for _, attachment := range attachments {
|
|
||||||
for _, action := range attachment.Actions {
|
|
||||||
if action.Id == "" {
|
|
||||||
action.Id = NewId()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var markdownDestinationEscaper = strings.NewReplacer(
|
var markdownDestinationEscaper = strings.NewReplacer(
|
||||||
`\`, `\\`,
|
`\`, `\\`,
|
||||||
`<`, `\<`,
|
`<`, `\<`,
|
||||||
@@ -524,12 +418,6 @@ func (o *PostEphemeral) ToUnsanitizedJson() string {
|
|||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DoPostActionRequestFromJson(data io.Reader) *DoPostActionRequest {
|
|
||||||
var o *DoPostActionRequest
|
|
||||||
json.NewDecoder(data).Decode(&o)
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
// RewriteImageURLs takes a message and returns a copy that has all of the image URLs replaced
|
// RewriteImageURLs takes a message and returns a copy that has all of the image URLs replaced
|
||||||
// according to the function f. For each image URL, f will be invoked, and the resulting markdown
|
// according to the function f. For each image URL, f will be invoked, and the resulting markdown
|
||||||
// will contain the URL returned by that invocation instead.
|
// will contain the URL returned by that invocation instead.
|
||||||
|
|||||||
@@ -25,34 +25,6 @@ func TestPostFromJsonError(t *testing.T) {
|
|||||||
assert.Nil(t, ro)
|
assert.Nil(t, ro)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPostActionIntegrationRequestToJson(t *testing.T) {
|
|
||||||
o := PostActionIntegrationRequest{UserId: NewId(), Context: StringInterface{"a": "abc"}}
|
|
||||||
j := o.ToJson()
|
|
||||||
ro := PostActionIntegrationRequesteFromJson(strings.NewReader(j))
|
|
||||||
|
|
||||||
assert.NotNil(t, ro)
|
|
||||||
assert.Equal(t, o, *ro)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPostActionIntegrationRequestFromJsonError(t *testing.T) {
|
|
||||||
ro := PostActionIntegrationRequesteFromJson(strings.NewReader(""))
|
|
||||||
assert.Nil(t, ro)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPostActionIntegrationResponseToJson(t *testing.T) {
|
|
||||||
o := PostActionIntegrationResponse{Update: &Post{Id: NewId(), Message: NewId()}, EphemeralText: NewId()}
|
|
||||||
j := o.ToJson()
|
|
||||||
ro := PostActionIntegrationResponseFromJson(strings.NewReader(j))
|
|
||||||
|
|
||||||
assert.NotNil(t, ro)
|
|
||||||
assert.Equal(t, o, *ro)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPostActionIntegrationResponseFromJsonError(t *testing.T) {
|
|
||||||
ro := PostActionIntegrationResponseFromJson(strings.NewReader(""))
|
|
||||||
assert.Nil(t, ro)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPostIsValid(t *testing.T) {
|
func TestPostIsValid(t *testing.T) {
|
||||||
o := Post{}
|
o := Post{}
|
||||||
maxPostSize := 10000
|
maxPostSize := 10000
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ const (
|
|||||||
WEBSOCKET_EVENT_ROLE_UPDATED = "role_updated"
|
WEBSOCKET_EVENT_ROLE_UPDATED = "role_updated"
|
||||||
WEBSOCKET_EVENT_LICENSE_CHANGED = "license_changed"
|
WEBSOCKET_EVENT_LICENSE_CHANGED = "license_changed"
|
||||||
WEBSOCKET_EVENT_CONFIG_CHANGED = "config_changed"
|
WEBSOCKET_EVENT_CONFIG_CHANGED = "config_changed"
|
||||||
|
WEBSOCKET_EVENT_OPEN_DIALOG = "open_dialog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebSocketMessage interface {
|
type WebSocketMessage interface {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
package plugin
|
package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/hashicorp/go-plugin"
|
plugin "github.com/hashicorp/go-plugin"
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"github.com/mattermost/mattermost-server/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,6 +34,16 @@ type API interface {
|
|||||||
// SaveConfig sets the given config and persists the changes
|
// SaveConfig sets the given config and persists the changes
|
||||||
SaveConfig(config *model.Config) *model.AppError
|
SaveConfig(config *model.Config) *model.AppError
|
||||||
|
|
||||||
|
// GetPluginConfig fetches the currently persisted config of plugin
|
||||||
|
//
|
||||||
|
// Minimum server version: 5.6
|
||||||
|
GetPluginConfig() map[string]interface{}
|
||||||
|
|
||||||
|
// SavePluginConfig sets the given config for plugin and persists the changes
|
||||||
|
//
|
||||||
|
// Minimum server version: 5.6
|
||||||
|
SavePluginConfig(config map[string]interface{}) *model.AppError
|
||||||
|
|
||||||
// GetServerVersion return the current Mattermost server version
|
// GetServerVersion return the current Mattermost server version
|
||||||
//
|
//
|
||||||
// Minimum server version: 5.4
|
// Minimum server version: 5.4
|
||||||
@@ -69,6 +79,11 @@ type API interface {
|
|||||||
// Minimum server version: 5.6
|
// Minimum server version: 5.6
|
||||||
GetTeamIcon(teamId string) ([]byte, *model.AppError)
|
GetTeamIcon(teamId string) ([]byte, *model.AppError)
|
||||||
|
|
||||||
|
// SetTeamIcon sets the Team Icon.
|
||||||
|
//
|
||||||
|
// Minimum server version: 5.6
|
||||||
|
SetTeamIcon(teamId string, data []byte) *model.AppError
|
||||||
|
|
||||||
// UpdateUser updates a user.
|
// UpdateUser updates a user.
|
||||||
UpdateUser(user *model.User) (*model.User, *model.AppError)
|
UpdateUser(user *model.User) (*model.User, *model.AppError)
|
||||||
|
|
||||||
@@ -134,7 +149,7 @@ type API interface {
|
|||||||
DeleteTeamMember(teamId, userId, requestorId string) *model.AppError
|
DeleteTeamMember(teamId, userId, requestorId string) *model.AppError
|
||||||
|
|
||||||
// GetTeamMembers returns the memberships of a specific team.
|
// GetTeamMembers returns the memberships of a specific team.
|
||||||
GetTeamMembers(teamId string, offset, limit int) ([]*model.TeamMember, *model.AppError)
|
GetTeamMembers(teamId string, page, perPage int) ([]*model.TeamMember, *model.AppError)
|
||||||
|
|
||||||
// GetTeamMember returns a specific membership.
|
// GetTeamMember returns a specific membership.
|
||||||
GetTeamMember(teamId, userId string) (*model.TeamMember, *model.AppError)
|
GetTeamMember(teamId, userId string) (*model.TeamMember, *model.AppError)
|
||||||
@@ -149,7 +164,7 @@ type API interface {
|
|||||||
DeleteChannel(channelId string) *model.AppError
|
DeleteChannel(channelId string) *model.AppError
|
||||||
|
|
||||||
// GetPublicChannelsForTeam gets a list of all channels.
|
// GetPublicChannelsForTeam gets a list of all channels.
|
||||||
GetPublicChannelsForTeam(teamId string, offset, limit int) (*model.ChannelList, *model.AppError)
|
GetPublicChannelsForTeam(teamId string, page, perPage int) ([]*model.Channel, *model.AppError)
|
||||||
|
|
||||||
// GetChannel gets a channel.
|
// GetChannel gets a channel.
|
||||||
GetChannel(channelId string) (*model.Channel, *model.AppError)
|
GetChannel(channelId string) (*model.Channel, *model.AppError)
|
||||||
@@ -324,6 +339,13 @@ type API interface {
|
|||||||
// Minimum server version: 5.6
|
// Minimum server version: 5.6
|
||||||
UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError)
|
UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError)
|
||||||
|
|
||||||
|
// OpenInteractiveDialog will open an interactive dialog on a user's client that
|
||||||
|
// generated the trigger ID. Used with interactive message buttons, menus
|
||||||
|
// and slash commands.
|
||||||
|
//
|
||||||
|
// Minimum server version: 5.6
|
||||||
|
OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppError
|
||||||
|
|
||||||
// Plugin Section
|
// Plugin Section
|
||||||
|
|
||||||
// GetPlugins will return a list of plugin manifests for currently active plugins.
|
// GetPlugins will return a list of plugin manifests for currently active plugins.
|
||||||
|
|||||||
@@ -656,6 +656,61 @@ func (s *apiRPCServer) SaveConfig(args *Z_SaveConfigArgs, returns *Z_SaveConfigR
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Z_GetPluginConfigArgs struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type Z_GetPluginConfigReturns struct {
|
||||||
|
A map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *apiRPCClient) GetPluginConfig() map[string]interface{} {
|
||||||
|
_args := &Z_GetPluginConfigArgs{}
|
||||||
|
_returns := &Z_GetPluginConfigReturns{}
|
||||||
|
if err := g.client.Call("Plugin.GetPluginConfig", _args, _returns); err != nil {
|
||||||
|
log.Printf("RPC call to GetPluginConfig API failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
return _returns.A
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *apiRPCServer) GetPluginConfig(args *Z_GetPluginConfigArgs, returns *Z_GetPluginConfigReturns) error {
|
||||||
|
if hook, ok := s.impl.(interface {
|
||||||
|
GetPluginConfig() map[string]interface{}
|
||||||
|
}); ok {
|
||||||
|
returns.A = hook.GetPluginConfig()
|
||||||
|
} else {
|
||||||
|
return encodableError(fmt.Errorf("API GetPluginConfig called but not implemented."))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Z_SavePluginConfigArgs struct {
|
||||||
|
A map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Z_SavePluginConfigReturns struct {
|
||||||
|
A *model.AppError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *apiRPCClient) SavePluginConfig(config map[string]interface{}) *model.AppError {
|
||||||
|
_args := &Z_SavePluginConfigArgs{config}
|
||||||
|
_returns := &Z_SavePluginConfigReturns{}
|
||||||
|
if err := g.client.Call("Plugin.SavePluginConfig", _args, _returns); err != nil {
|
||||||
|
log.Printf("RPC call to SavePluginConfig API failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
return _returns.A
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *apiRPCServer) SavePluginConfig(args *Z_SavePluginConfigArgs, returns *Z_SavePluginConfigReturns) error {
|
||||||
|
if hook, ok := s.impl.(interface {
|
||||||
|
SavePluginConfig(config map[string]interface{}) *model.AppError
|
||||||
|
}); ok {
|
||||||
|
returns.A = hook.SavePluginConfig(args.A)
|
||||||
|
} else {
|
||||||
|
return encodableError(fmt.Errorf("API SavePluginConfig called but not implemented."))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type Z_GetServerVersionArgs struct {
|
type Z_GetServerVersionArgs struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -916,6 +971,35 @@ func (s *apiRPCServer) GetTeamIcon(args *Z_GetTeamIconArgs, returns *Z_GetTeamIc
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Z_SetTeamIconArgs struct {
|
||||||
|
A string
|
||||||
|
B []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type Z_SetTeamIconReturns struct {
|
||||||
|
A *model.AppError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *apiRPCClient) SetTeamIcon(teamId string, data []byte) *model.AppError {
|
||||||
|
_args := &Z_SetTeamIconArgs{teamId, data}
|
||||||
|
_returns := &Z_SetTeamIconReturns{}
|
||||||
|
if err := g.client.Call("Plugin.SetTeamIcon", _args, _returns); err != nil {
|
||||||
|
log.Printf("RPC call to SetTeamIcon API failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
return _returns.A
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *apiRPCServer) SetTeamIcon(args *Z_SetTeamIconArgs, returns *Z_SetTeamIconReturns) error {
|
||||||
|
if hook, ok := s.impl.(interface {
|
||||||
|
SetTeamIcon(teamId string, data []byte) *model.AppError
|
||||||
|
}); ok {
|
||||||
|
returns.A = hook.SetTeamIcon(args.A, args.B)
|
||||||
|
} else {
|
||||||
|
return encodableError(fmt.Errorf("API SetTeamIcon called but not implemented."))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type Z_UpdateUserArgs struct {
|
type Z_UpdateUserArgs struct {
|
||||||
A *model.User
|
A *model.User
|
||||||
}
|
}
|
||||||
@@ -1427,8 +1511,8 @@ type Z_GetTeamMembersReturns struct {
|
|||||||
B *model.AppError
|
B *model.AppError
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *apiRPCClient) GetTeamMembers(teamId string, offset, limit int) ([]*model.TeamMember, *model.AppError) {
|
func (g *apiRPCClient) GetTeamMembers(teamId string, page, perPage int) ([]*model.TeamMember, *model.AppError) {
|
||||||
_args := &Z_GetTeamMembersArgs{teamId, offset, limit}
|
_args := &Z_GetTeamMembersArgs{teamId, page, perPage}
|
||||||
_returns := &Z_GetTeamMembersReturns{}
|
_returns := &Z_GetTeamMembersReturns{}
|
||||||
if err := g.client.Call("Plugin.GetTeamMembers", _args, _returns); err != nil {
|
if err := g.client.Call("Plugin.GetTeamMembers", _args, _returns); err != nil {
|
||||||
log.Printf("RPC call to GetTeamMembers API failed: %s", err.Error())
|
log.Printf("RPC call to GetTeamMembers API failed: %s", err.Error())
|
||||||
@@ -1438,7 +1522,7 @@ func (g *apiRPCClient) GetTeamMembers(teamId string, offset, limit int) ([]*mode
|
|||||||
|
|
||||||
func (s *apiRPCServer) GetTeamMembers(args *Z_GetTeamMembersArgs, returns *Z_GetTeamMembersReturns) error {
|
func (s *apiRPCServer) GetTeamMembers(args *Z_GetTeamMembersArgs, returns *Z_GetTeamMembersReturns) error {
|
||||||
if hook, ok := s.impl.(interface {
|
if hook, ok := s.impl.(interface {
|
||||||
GetTeamMembers(teamId string, offset, limit int) ([]*model.TeamMember, *model.AppError)
|
GetTeamMembers(teamId string, page, perPage int) ([]*model.TeamMember, *model.AppError)
|
||||||
}); ok {
|
}); ok {
|
||||||
returns.A, returns.B = hook.GetTeamMembers(args.A, args.B, args.C)
|
returns.A, returns.B = hook.GetTeamMembers(args.A, args.B, args.C)
|
||||||
} else {
|
} else {
|
||||||
@@ -1572,12 +1656,12 @@ type Z_GetPublicChannelsForTeamArgs struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Z_GetPublicChannelsForTeamReturns struct {
|
type Z_GetPublicChannelsForTeamReturns struct {
|
||||||
A *model.ChannelList
|
A []*model.Channel
|
||||||
B *model.AppError
|
B *model.AppError
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *apiRPCClient) GetPublicChannelsForTeam(teamId string, offset, limit int) (*model.ChannelList, *model.AppError) {
|
func (g *apiRPCClient) GetPublicChannelsForTeam(teamId string, page, perPage int) ([]*model.Channel, *model.AppError) {
|
||||||
_args := &Z_GetPublicChannelsForTeamArgs{teamId, offset, limit}
|
_args := &Z_GetPublicChannelsForTeamArgs{teamId, page, perPage}
|
||||||
_returns := &Z_GetPublicChannelsForTeamReturns{}
|
_returns := &Z_GetPublicChannelsForTeamReturns{}
|
||||||
if err := g.client.Call("Plugin.GetPublicChannelsForTeam", _args, _returns); err != nil {
|
if err := g.client.Call("Plugin.GetPublicChannelsForTeam", _args, _returns); err != nil {
|
||||||
log.Printf("RPC call to GetPublicChannelsForTeam API failed: %s", err.Error())
|
log.Printf("RPC call to GetPublicChannelsForTeam API failed: %s", err.Error())
|
||||||
@@ -1587,7 +1671,7 @@ func (g *apiRPCClient) GetPublicChannelsForTeam(teamId string, offset, limit int
|
|||||||
|
|
||||||
func (s *apiRPCServer) GetPublicChannelsForTeam(args *Z_GetPublicChannelsForTeamArgs, returns *Z_GetPublicChannelsForTeamReturns) error {
|
func (s *apiRPCServer) GetPublicChannelsForTeam(args *Z_GetPublicChannelsForTeamArgs, returns *Z_GetPublicChannelsForTeamReturns) error {
|
||||||
if hook, ok := s.impl.(interface {
|
if hook, ok := s.impl.(interface {
|
||||||
GetPublicChannelsForTeam(teamId string, offset, limit int) (*model.ChannelList, *model.AppError)
|
GetPublicChannelsForTeam(teamId string, page, perPage int) ([]*model.Channel, *model.AppError)
|
||||||
}); ok {
|
}); ok {
|
||||||
returns.A, returns.B = hook.GetPublicChannelsForTeam(args.A, args.B, args.C)
|
returns.A, returns.B = hook.GetPublicChannelsForTeam(args.A, args.B, args.C)
|
||||||
} else {
|
} else {
|
||||||
@@ -2786,6 +2870,34 @@ func (s *apiRPCServer) UploadFile(args *Z_UploadFileArgs, returns *Z_UploadFileR
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Z_OpenInteractiveDialogArgs struct {
|
||||||
|
A model.OpenDialogRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
type Z_OpenInteractiveDialogReturns struct {
|
||||||
|
A *model.AppError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *apiRPCClient) OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppError {
|
||||||
|
_args := &Z_OpenInteractiveDialogArgs{dialog}
|
||||||
|
_returns := &Z_OpenInteractiveDialogReturns{}
|
||||||
|
if err := g.client.Call("Plugin.OpenInteractiveDialog", _args, _returns); err != nil {
|
||||||
|
log.Printf("RPC call to OpenInteractiveDialog API failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
return _returns.A
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *apiRPCServer) OpenInteractiveDialog(args *Z_OpenInteractiveDialogArgs, returns *Z_OpenInteractiveDialogReturns) error {
|
||||||
|
if hook, ok := s.impl.(interface {
|
||||||
|
OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppError
|
||||||
|
}); ok {
|
||||||
|
returns.A = hook.OpenInteractiveDialog(args.A)
|
||||||
|
} else {
|
||||||
|
return encodableError(fmt.Errorf("API OpenInteractiveDialog called but not implemented."))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type Z_GetPluginsArgs struct {
|
type Z_GetPluginsArgs struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -581,6 +581,20 @@ func (_m *API) GetConfig() *model.Config {
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPluginConfig provides a mock function with given fields:
|
||||||
|
func (_m *API) GetPluginConfig() map[string]interface{} {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 map[string]interface{}
|
||||||
|
if rf, ok := ret.Get(0).(func() map[string]interface{}); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// GetDirectChannel provides a mock function with given fields: userId1, userId2
|
// GetDirectChannel provides a mock function with given fields: userId1, userId2
|
||||||
func (_m *API) GetDirectChannel(userId1 string, userId2 string) (*model.Channel, *model.AppError) {
|
func (_m *API) GetDirectChannel(userId1 string, userId2 string) (*model.Channel, *model.AppError) {
|
||||||
ret := _m.Called(userId1, userId2)
|
ret := _m.Called(userId1, userId2)
|
||||||
@@ -1036,22 +1050,22 @@ func (_m *API) GetProfileImage(userId string) ([]byte, *model.AppError) {
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPublicChannelsForTeam provides a mock function with given fields: teamId, offset, limit
|
// GetPublicChannelsForTeam provides a mock function with given fields: teamId, page, perPage
|
||||||
func (_m *API) GetPublicChannelsForTeam(teamId string, offset int, limit int) (*model.ChannelList, *model.AppError) {
|
func (_m *API) GetPublicChannelsForTeam(teamId string, page int, perPage int) ([]*model.Channel, *model.AppError) {
|
||||||
ret := _m.Called(teamId, offset, limit)
|
ret := _m.Called(teamId, page, perPage)
|
||||||
|
|
||||||
var r0 *model.ChannelList
|
var r0 []*model.Channel
|
||||||
if rf, ok := ret.Get(0).(func(string, int, int) *model.ChannelList); ok {
|
if rf, ok := ret.Get(0).(func(string, int, int) []*model.Channel); ok {
|
||||||
r0 = rf(teamId, offset, limit)
|
r0 = rf(teamId, page, perPage)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(0) != nil {
|
if ret.Get(0) != nil {
|
||||||
r0 = ret.Get(0).(*model.ChannelList)
|
r0 = ret.Get(0).([]*model.Channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var r1 *model.AppError
|
var r1 *model.AppError
|
||||||
if rf, ok := ret.Get(1).(func(string, int, int) *model.AppError); ok {
|
if rf, ok := ret.Get(1).(func(string, int, int) *model.AppError); ok {
|
||||||
r1 = rf(teamId, offset, limit)
|
r1 = rf(teamId, page, perPage)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(1) != nil {
|
if ret.Get(1) != nil {
|
||||||
r1 = ret.Get(1).(*model.AppError)
|
r1 = ret.Get(1).(*model.AppError)
|
||||||
@@ -1225,13 +1239,13 @@ func (_m *API) GetTeamMember(teamId string, userId string) (*model.TeamMember, *
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTeamMembers provides a mock function with given fields: teamId, offset, limit
|
// GetTeamMembers provides a mock function with given fields: teamId, page, perPage
|
||||||
func (_m *API) GetTeamMembers(teamId string, offset int, limit int) ([]*model.TeamMember, *model.AppError) {
|
func (_m *API) GetTeamMembers(teamId string, page int, perPage int) ([]*model.TeamMember, *model.AppError) {
|
||||||
ret := _m.Called(teamId, offset, limit)
|
ret := _m.Called(teamId, page, perPage)
|
||||||
|
|
||||||
var r0 []*model.TeamMember
|
var r0 []*model.TeamMember
|
||||||
if rf, ok := ret.Get(0).(func(string, int, int) []*model.TeamMember); ok {
|
if rf, ok := ret.Get(0).(func(string, int, int) []*model.TeamMember); ok {
|
||||||
r0 = rf(teamId, offset, limit)
|
r0 = rf(teamId, page, perPage)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(0) != nil {
|
if ret.Get(0) != nil {
|
||||||
r0 = ret.Get(0).([]*model.TeamMember)
|
r0 = ret.Get(0).([]*model.TeamMember)
|
||||||
@@ -1240,7 +1254,7 @@ func (_m *API) GetTeamMembers(teamId string, offset int, limit int) ([]*model.Te
|
|||||||
|
|
||||||
var r1 *model.AppError
|
var r1 *model.AppError
|
||||||
if rf, ok := ret.Get(1).(func(string, int, int) *model.AppError); ok {
|
if rf, ok := ret.Get(1).(func(string, int, int) *model.AppError); ok {
|
||||||
r1 = rf(teamId, offset, limit)
|
r1 = rf(teamId, page, perPage)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(1) != nil {
|
if ret.Get(1) != nil {
|
||||||
r1 = ret.Get(1).(*model.AppError)
|
r1 = ret.Get(1).(*model.AppError)
|
||||||
@@ -1727,6 +1741,22 @@ func (_m *API) LogWarn(msg string, keyValuePairs ...interface{}) {
|
|||||||
_m.Called(_ca...)
|
_m.Called(_ca...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenInteractiveDialog provides a mock function with given fields: dialog
|
||||||
|
func (_m *API) OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppError {
|
||||||
|
ret := _m.Called(dialog)
|
||||||
|
|
||||||
|
var r0 *model.AppError
|
||||||
|
if rf, ok := ret.Get(0).(func(model.OpenDialogRequest) *model.AppError); ok {
|
||||||
|
r0 = rf(dialog)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*model.AppError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// PublishWebSocketEvent provides a mock function with given fields: event, payload, broadcast
|
// PublishWebSocketEvent provides a mock function with given fields: event, payload, broadcast
|
||||||
func (_m *API) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *model.WebsocketBroadcast) {
|
func (_m *API) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *model.WebsocketBroadcast) {
|
||||||
_m.Called(event, payload, broadcast)
|
_m.Called(event, payload, broadcast)
|
||||||
@@ -1819,6 +1849,22 @@ func (_m *API) SaveConfig(config *model.Config) *model.AppError {
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SavePluginConfig provides a mock function with given fields: pluginConfig
|
||||||
|
func (_m *API) SavePluginConfig(pluginConfig map[string]interface{}) *model.AppError {
|
||||||
|
ret := _m.Called(pluginConfig)
|
||||||
|
|
||||||
|
var r0 *model.AppError
|
||||||
|
if rf, ok := ret.Get(0).(func(map[string]interface{}) *model.AppError); ok {
|
||||||
|
r0 = rf(pluginConfig)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*model.AppError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// SearchChannels provides a mock function with given fields: teamId, term
|
// SearchChannels provides a mock function with given fields: teamId, term
|
||||||
func (_m *API) SearchChannels(teamId string, term string) (*model.ChannelList, *model.AppError) {
|
func (_m *API) SearchChannels(teamId string, term string) (*model.ChannelList, *model.AppError) {
|
||||||
ret := _m.Called(teamId, term)
|
ret := _m.Called(teamId, term)
|
||||||
@@ -1876,6 +1922,22 @@ func (_m *API) SetProfileImage(userId string, data []byte) *model.AppError {
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetTeamIcon provides a mock function with given fields: teamId, data
|
||||||
|
func (_m *API) SetTeamIcon(teamId string, data []byte) *model.AppError {
|
||||||
|
ret := _m.Called(teamId, data)
|
||||||
|
|
||||||
|
var r0 *model.AppError
|
||||||
|
if rf, ok := ret.Get(0).(func(string, []byte) *model.AppError); ok {
|
||||||
|
r0 = rf(teamId, data)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*model.AppError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// UnregisterCommand provides a mock function with given fields: teamId, trigger
|
// UnregisterCommand provides a mock function with given fields: teamId, trigger
|
||||||
func (_m *API) UnregisterCommand(teamId string, trigger string) error {
|
func (_m *API) UnregisterCommand(teamId string, trigger string) error {
|
||||||
ret := _m.Called(teamId, trigger)
|
ret := _m.Called(teamId, trigger)
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user