Merge branch 'master' into post-metadata

Этот коммит содержится в:
Harrison Healey
2018-11-19 16:51:56 -05:00
родитель 23c8950312 8cfca681b0
Коммит 8dc865b917
31 изменённых файлов: 1956 добавлений и 557 удалений

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

@@ -5,6 +5,7 @@ package model
import (
"bytes"
"encoding/json"
"fmt"
"io"
"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) {
requestBody := map[string]interface{}{"terms": terms, "is_or_search": isOrSearch}
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
// 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"`
RootId string `json:"root_id"`
ParentId string `json:"parent_id"`
TriggerId string `json:"trigger_id,omitempty"`
Command string `json:"command"`
SiteURL string `json:"-"`
T goi18n.TranslateFunc `json:"-"`

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

@@ -18,14 +18,16 @@ const (
)
type CommandResponse struct {
ResponseType string `json:"response_type"`
Text string `json:"text"`
Username string `json:"username"`
IconURL string `json:"icon_url"`
Type string `json:"type"`
Props StringInterface `json:"props"`
GotoLocation string `json:"goto_location"`
Attachments []*SlackAttachment `json:"attachments"`
ResponseType string `json:"response_type"`
Text string `json:"text"`
Username string `json:"username"`
IconURL string `json:"icon_url"`
Type string `json:"type"`
Props StringInterface `json:"props"`
GotoLocation string `json:"goto_location"`
TriggerId string `json:"trigger_id"`
Attachments []*SlackAttachment `json:"attachments"`
ExtraResponses []*CommandResponse `json:"extra_responses"`
}
func (o *CommandResponse) ToJson() string {
@@ -63,5 +65,11 @@ func CommandResponseFromJson(data io.Reader) (*CommandResponse, error) {
o.Attachments = StringifySlackFieldValue(o.Attachments)
if o.ExtraResponses != nil {
for _, resp := range o.ExtraResponses {
resp.Attachments = StringifySlackFieldValue(resp.Attachments)
}
}
return &o, nil
}

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

@@ -131,6 +131,71 @@ func TestCommandResponseFromJson(t *testing.T) {
},
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 {

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 Обычный файл
Просмотреть файл

@@ -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)
}

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

@@ -50,8 +50,6 @@ const (
PROPS_ADD_CHANNEL_MEMBER = "add_channel_member"
POST_PROPS_ADDED_USER_ID = "addedUserId"
POST_PROPS_DELETE_BY = "deleteBy"
POST_ACTION_TYPE_BUTTON = "button"
POST_ACTION_TYPE_SELECT = "select"
)
type Post struct {
@@ -135,44 +133,6 @@ type PostForIndexing struct {
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
func (o *Post) Clone() *Post {
copy := *o
@@ -416,34 +376,6 @@ func (o *Post) ChannelMentions() []string {
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 {
if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok {
return attachments
@@ -462,44 +394,6 @@ func (o *Post) Attachments() []*SlackAttachment {
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(
`\`, `\\`,
`<`, `\<`,
@@ -524,12 +418,6 @@ func (o *PostEphemeral) ToUnsanitizedJson() string {
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
// 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.

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

@@ -25,34 +25,6 @@ func TestPostFromJsonError(t *testing.T) {
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) {
o := Post{}
maxPostSize := 10000

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

@@ -49,6 +49,7 @@ const (
WEBSOCKET_EVENT_ROLE_UPDATED = "role_updated"
WEBSOCKET_EVENT_LICENSE_CHANGED = "license_changed"
WEBSOCKET_EVENT_CONFIG_CHANGED = "config_changed"
WEBSOCKET_EVENT_OPEN_DIALOG = "open_dialog"
)
type WebSocketMessage interface {