diff --git a/server/channels/api4/integration_action_test.go b/server/channels/api4/integration_action_test.go index bfda9530d4..2d2a3c597c 100644 --- a/server/channels/api4/integration_action_test.go +++ b/server/channels/api4/integration_action_test.go @@ -39,7 +39,7 @@ func (th *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { assert.NotEmpty(th.t, poir.TeamName) assert.NotEmpty(th.t, poir.PostId) assert.NotEmpty(th.t, poir.TriggerId) - assert.Equal(th.t, "button", poir.Type) + assert.Equal(th.t, model.PostActionTypeButton, poir.Type) assert.Equal(th.t, "test-value", poir.Context["test-key"]) _, err = w.Write([]byte("{}")) require.NoError(th.t, err) @@ -118,7 +118,7 @@ func TestPostActionCookies(t *testing.T) { CreateAt: model.GetMillis(), UpdateAt: model.GetMillis(), Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Title: "some-title", TitleLink: "https://some-url.com", diff --git a/server/channels/api4/post_test.go b/server/channels/api4/post_test.go index a7bfab09b8..76facb727f 100644 --- a/server/channels/api4/post_test.go +++ b/server/channels/api4/post_test.go @@ -1420,7 +1420,7 @@ func TestUpdatePost(t *testing.T) { ChannelId: channel.Id, Message: "zz" + model.NewId() + " update post 3", } - up4.AddProp("attachments", []model.SlackAttachment{ + up4.AddProp(model.PostPropsAttachments, []model.SlackAttachment{ { Text: "Hello World", }, @@ -1772,12 +1772,12 @@ func TestPatchPost(t *testing.T) { Text: "Hello World", }, } - patch2.Props = &model.StringInterface{"attachments": attachments} + patch2.Props = &model.StringInterface{model.PostPropsAttachments: attachments} var rpost2 *model.Post rpost2, _, err = client.PatchPost(context.Background(), post.Id, patch2) require.NoError(t, err) - assert.NotEmpty(t, rpost2.GetProp("attachments")) + assert.NotEmpty(t, rpost2.GetProp(model.PostPropsAttachments)) assert.NotEqual(t, rpost.EditAt, rpost2.EditAt) }) @@ -4682,7 +4682,7 @@ func TestGetPostStripActionIntegrations(t *testing.T) { ChannelId: th.BasicChannel.Id, Message: "with slack attachment action", } - post.AddProp("attachments", []*model.SlackAttachment{ + post.AddProp(model.PostPropsAttachments, []*model.SlackAttachment{ { Text: "Slack Attachment Text", Fields: []*model.SlackAttachmentField{ @@ -4694,7 +4694,7 @@ func TestGetPostStripActionIntegrations(t *testing.T) { }, Actions: []*model.PostAction{ { - Type: "button", + Type: model.PostActionTypeButton, Name: "test-name", Integration: &model.PostActionIntegration{ URL: "https://test.test/action", @@ -4713,7 +4713,7 @@ func TestGetPostStripActionIntegrations(t *testing.T) { actualPost, _, err := client.GetPost(context.Background(), rpost.Id, "") require.NoError(t, err) - attachments, _ := actualPost.Props["attachments"].([]any) + attachments, _ := actualPost.Props[model.PostPropsAttachments].([]any) require.Equal(t, 1, len(attachments)) att, _ := attachments[0].(map[string]any) require.NotNil(t, att) diff --git a/server/channels/app/command.go b/server/channels/app/command.go index 09c1e6749b..d855005c44 100644 --- a/server/channels/app/command.go +++ b/server/channels/app/command.go @@ -618,28 +618,28 @@ func (a *App) HandleCommandResponsePost(c request.CTX, command *model.Command, a if *a.Config().ServiceSettings.EnablePostUsernameOverride { if command.Username != "" { - post.AddProp("override_username", command.Username) + post.AddProp(model.PostPropsOverrideUsername, command.Username) isBotPost = true } else if response.Username != "" { - post.AddProp("override_username", response.Username) + post.AddProp(model.PostPropsOverrideUsername, response.Username) isBotPost = true } } if *a.Config().ServiceSettings.EnablePostIconOverride { if command.IconURL != "" { - post.AddProp("override_icon_url", command.IconURL) + post.AddProp(model.PostPropsOverrideIconURL, command.IconURL) isBotPost = true } else if response.IconURL != "" { - post.AddProp("override_icon_url", response.IconURL) + post.AddProp(model.PostPropsOverrideIconURL, response.IconURL) isBotPost = true } else { - post.AddProp("override_icon_url", "") + post.AddProp(model.PostPropsOverrideIconURL, "") } } if isBotPost { - post.AddProp("from_webhook", "true") + post.AddProp(model.PostPropsFromWebhook, "true") } // Process Slack text replacements if the response does not contain "skip_slack_parsing": true. diff --git a/server/channels/app/export_test.go b/server/channels/app/export_test.go index e84e690544..8ce29d278c 100644 --- a/server/channels/app/export_test.go +++ b/server/channels/app/export_test.go @@ -623,7 +623,7 @@ func TestExportPostWithProps(t *testing.T) { ChannelId: dmChannel.Id, Message: "aa" + model.NewId() + "a", Props: map[string]any{ - "attachments": attachments, + model.PostPropsAttachments: attachments, }, UserId: th1.BasicUser.Id, } @@ -634,7 +634,7 @@ func TestExportPostWithProps(t *testing.T) { ChannelId: gmChannel.Id, Message: "dd" + model.NewId() + "a", Props: map[string]any{ - "attachments": attachments, + model.PostPropsAttachments: attachments, }, UserId: th1.BasicUser.Id, } @@ -673,8 +673,8 @@ func TestExportPostWithProps(t *testing.T) { assert.Len(t, posts, 2) assert.ElementsMatch(t, gmMembers, *posts[0].ChannelMembers) assert.ElementsMatch(t, dmMembers, *posts[1].ChannelMembers) - assert.Contains(t, posts[0].Props["attachments"].([]any)[0], "footer") - assert.Contains(t, posts[1].Props["attachments"].([]any)[0], "footer") + assert.Contains(t, posts[0].Props[model.PostPropsAttachments].([]any)[0], "footer") + assert.Contains(t, posts[1].Props[model.PostPropsAttachments].([]any)[0], "footer") } func TestExportUserCustomStatus(t *testing.T) { diff --git a/server/channels/app/import_functions.go b/server/channels/app/import_functions.go index 8d00c4e171..bc6a86196c 100644 --- a/server/channels/app/import_functions.go +++ b/server/channels/app/import_functions.go @@ -1455,7 +1455,7 @@ func (a *App) importReplies(rctx request.CTX, data []imports.ReplyImportData, po } if len(postsForCreateList) > 0 { - postsCreated, _, err := a.Srv().Store().Post().SaveMultiple(postsForCreateList) + postsCreated, _, err := a.Srv().Store().Post().SaveMultiple(rctx, postsForCreateList) if err != nil { var appErr *model.AppError var invErr *store.ErrInvalidInput @@ -1478,7 +1478,7 @@ func (a *App) importReplies(rctx request.CTX, data []imports.ReplyImportData, po } } - if _, _, nErr := a.Srv().Store().Post().OverwriteMultiple(postsForOverwriteList); nErr != nil { + if _, _, nErr := a.Srv().Store().Post().OverwriteMultiple(rctx, postsForOverwriteList); nErr != nil { return model.NewAppError("importReplies", "app.post.overwrite.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } @@ -1892,7 +1892,7 @@ func (a *App) importMultiplePostLines(rctx request.CTX, lines []imports.LineImpo } if len(postsForCreateList) > 0 { - _, idx, nErr := a.Srv().Store().Post().SaveMultiple(postsForCreateList) + _, idx, nErr := a.Srv().Store().Post().SaveMultiple(rctx, postsForCreateList) if nErr != nil { var appErr *model.AppError var invErr *store.ErrInvalidInput @@ -1946,7 +1946,7 @@ func (a *App) importMultiplePostLines(rctx request.CTX, lines []imports.LineImpo } } - if _, idx, err := a.Srv().Store().Post().OverwriteMultiple(postsForOverwriteList); err != nil { + if _, idx, err := a.Srv().Store().Post().OverwriteMultiple(rctx, postsForOverwriteList); err != nil { if idx != -1 && idx < len(postsForOverwriteList) { post := postsForOverwriteList[idx] if lineNumber, ok := postsForOverwriteMap[getPostStrID(post)]; ok { @@ -2411,7 +2411,7 @@ func (a *App) importMultipleDirectPostLines(rctx request.CTX, lines []imports.Li } if len(postsForCreateList) > 0 { - if _, idx, err := a.Srv().Store().Post().SaveMultiple(postsForCreateList); err != nil { + if _, idx, err := a.Srv().Store().Post().SaveMultiple(rctx, postsForCreateList); err != nil { var appErr *model.AppError var invErr *store.ErrInvalidInput var retErr *model.AppError @@ -2459,7 +2459,7 @@ func (a *App) importMultipleDirectPostLines(rctx request.CTX, lines []imports.Li } } - if _, idx, err := a.Srv().Store().Post().OverwriteMultiple(postsForOverwriteList); err != nil { + if _, idx, err := a.Srv().Store().Post().OverwriteMultiple(rctx, postsForOverwriteList); err != nil { if idx != -1 && idx < len(postsForOverwriteList) { post := postsForOverwriteList[idx] if lineNumber, ok := postsForOverwriteMap[getPostStrID(post)]; ok { diff --git a/server/channels/app/integration_action_test.go b/server/channels/app/integration_action_test.go index e6e02d49d2..c8fd964f95 100644 --- a/server/channels/app/integration_action_test.go +++ b/server/channels/app/integration_action_test.go @@ -42,16 +42,16 @@ func TestPostActionInvalidURL(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeButton, + Name: "action", Integration: &model.PostActionIntegration{ URL: ":test", }, - Name: "action", - Type: "some_type", }, }, }, @@ -61,7 +61,7 @@ func TestPostActionInvalidURL(t *testing.T) { post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true) require.Nil(t, err) - attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) require.NotEmpty(t, attachments[0].Actions) require.NotEmpty(t, attachments[0].Actions[0].Id) @@ -90,11 +90,14 @@ func TestPostActionEmptyResponse(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeSelect, + Name: "action", + DataSource: model.PostActionDataSourceUsers, Integration: &model.PostActionIntegration{ Context: model.StringInterface{ "s": "foo", @@ -102,9 +105,6 @@ func TestPostActionEmptyResponse(t *testing.T) { }, URL: ts.URL, }, - Name: "action", - Type: "some_type", - DataSource: "some_source", }, }, }, @@ -115,7 +115,7 @@ func TestPostActionEmptyResponse(t *testing.T) { post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true) require.Nil(t, err) - attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) _, err = th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil) @@ -134,11 +134,14 @@ func TestPostActionEmptyResponse(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeSelect, + Name: "action", + DataSource: model.PostActionDataSourceUsers, Integration: &model.PostActionIntegration{ Context: model.StringInterface{ "s": "foo", @@ -146,9 +149,6 @@ func TestPostActionEmptyResponse(t *testing.T) { }, URL: ts.URL, }, - Name: "action", - Type: "some_type", - DataSource: "some_source", }, }, }, @@ -159,7 +159,7 @@ func TestPostActionEmptyResponse(t *testing.T) { post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true) require.Nil(t, err) - attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) th.App.UpdateConfig(func(cfg *model.Config) { @@ -209,23 +209,28 @@ func TestPostAction(t *testing.T) { jsonErr := json.NewDecoder(r.Body).Decode(&request) assert.NoError(t, jsonErr) - assert.Equal(t, request.UserId, th.BasicUser.Id) - assert.Equal(t, request.UserName, th.BasicUser.Username) - assert.Equal(t, request.ChannelId, channel.Id) - assert.Equal(t, request.ChannelName, channel.Name) + assert.Equal(t, th.BasicUser.Id, request.UserId) + assert.Equal(t, th.BasicUser.Username, request.UserName) + assert.Equal(t, channel.Id, request.ChannelId) + assert.Equal(t, channel.Name, request.ChannelName) if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup { assert.Empty(t, request.TeamId) assert.Empty(t, request.TeamName) } else { - assert.Equal(t, request.TeamId, th.BasicTeam.Id) - assert.Equal(t, request.TeamName, th.BasicTeam.Name) + assert.Equal(t, th.BasicTeam.Id, request.TeamId) + assert.Equal(t, th.BasicTeam.Name, request.TeamName) } assert.True(t, request.TriggerId != "") if request.Type == model.PostActionTypeSelect { - assert.Equal(t, request.DataSource, "some_source") - assert.Equal(t, request.Context["selected_option"], "selected") + if selectedOption, ok := request.Context["selected_option"]; ok { + // If something was selected, confirm that the data source and selected option are present + assert.Equal(t, model.PostActionDataSourceUsers, request.DataSource) + assert.Equal(t, "selected", selectedOption) + } else { + assert.Empty(t, request.DataSource) + } } else { - assert.Equal(t, request.DataSource, "") + assert.Equal(t, "", request.DataSource) } assert.Equal(t, "foo", request.Context["s"]) assert.EqualValues(t, 3, request.Context["n"]) @@ -239,11 +244,14 @@ func TestPostAction(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeSelect, + Name: "action", + DataSource: model.PostActionDataSourceUsers, Integration: &model.PostActionIntegration{ Context: model.StringInterface{ "s": "foo", @@ -251,9 +259,6 @@ func TestPostAction(t *testing.T) { }, URL: ts.URL, }, - Name: "action", - Type: "some_type", - DataSource: "some_source", }, }, }, @@ -264,7 +269,7 @@ func TestPostAction(t *testing.T) { post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true) require.Nil(t, err) - attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) require.NotEmpty(t, attachments[0].Actions) @@ -276,11 +281,14 @@ func TestPostAction(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeSelect, + Name: "action", + DataSource: model.PostActionDataSourceUsers, Integration: &model.PostActionIntegration{ Context: model.StringInterface{ "s": "foo", @@ -288,9 +296,6 @@ func TestPostAction(t *testing.T) { }, URL: ts.URL, }, - Name: "action", - Type: model.PostActionTypeSelect, - DataSource: "some_source", }, }, }, @@ -301,7 +306,7 @@ func TestPostAction(t *testing.T) { post2, err := th.App.CreatePostAsUser(th.Context, &menuPost, "", true) require.Nil(t, err) - attachments2, ok := post2.GetProp("attachments").([]*model.SlackAttachment) + attachments2, ok := post2.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) require.NotEmpty(t, attachments2[0].Actions) @@ -310,15 +315,15 @@ func TestPostAction(t *testing.T) { clientTriggerID, err := th.App.DoPostActionWithCookie(th.Context, post.Id, "notavalidid", th.BasicUser.Id, "", nil) require.NotNil(t, err) assert.Equal(t, http.StatusNotFound, err.StatusCode) - assert.True(t, clientTriggerID == "") + assert.Len(t, clientTriggerID, 0) clientTriggerID, err = th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil) require.Nil(t, err) - assert.True(t, len(clientTriggerID) == 26) + assert.Len(t, clientTriggerID, 26) clientTriggerID, err = th.App.DoPostActionWithCookie(th.Context, post2.Id, attachments2[0].Actions[0].Id, th.BasicUser.Id, "selected", nil) require.Nil(t, err) - assert.True(t, len(clientTriggerID) == 26) + assert.Len(t, clientTriggerID, 26) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "" @@ -334,11 +339,14 @@ func TestPostAction(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeSelect, + Name: "action", + DataSource: model.PostActionDataSourceUsers, Integration: &model.PostActionIntegration{ Context: model.StringInterface{ "s": "foo", @@ -346,9 +354,6 @@ func TestPostAction(t *testing.T) { }, URL: ts.URL + "/plugins/myplugin/myaction", }, - Name: "action", - Type: "some_type", - DataSource: "some_source", }, }, }, @@ -359,7 +364,7 @@ func TestPostAction(t *testing.T) { postplugin, err := th.App.CreatePostAsUser(th.Context, &interactivePostPlugin, "", true) require.Nil(t, err) - attachmentsPlugin, ok := postplugin.GetProp("attachments").([]*model.SlackAttachment) + attachmentsPlugin, ok := postplugin.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) _, err = th.App.DoPostActionWithCookie(th.Context, postplugin.Id, attachmentsPlugin[0].Actions[0].Id, th.BasicUser.Id, "", nil) @@ -382,11 +387,14 @@ func TestPostAction(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeSelect, + Name: "action", + DataSource: model.PostActionDataSourceUsers, Integration: &model.PostActionIntegration{ Context: model.StringInterface{ "s": "foo", @@ -394,9 +402,6 @@ func TestPostAction(t *testing.T) { }, URL: "http://127.1.1.1/plugins/myplugin/myaction", }, - Name: "action", - Type: "some_type", - DataSource: "some_source", }, }, }, @@ -407,7 +412,7 @@ func TestPostAction(t *testing.T) { postSiteURL, err := th.App.CreatePostAsUser(th.Context, &interactivePostSiteURL, "", true) require.Nil(t, err) - attachmentsSiteURL, ok := postSiteURL.GetProp("attachments").([]*model.SlackAttachment) + attachmentsSiteURL, ok := postSiteURL.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) _, err = th.App.DoPostActionWithCookie(th.Context, postSiteURL.Id, attachmentsSiteURL[0].Actions[0].Id, th.BasicUser.Id, "", nil) @@ -424,11 +429,14 @@ func TestPostAction(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeSelect, + Name: "action", + DataSource: model.PostActionDataSourceUsers, Integration: &model.PostActionIntegration{ Context: model.StringInterface{ "s": "foo", @@ -436,9 +444,6 @@ func TestPostAction(t *testing.T) { }, URL: ts.URL + "/subpath/plugins/myplugin/myaction", }, - Name: "action", - Type: "some_type", - DataSource: "some_source", }, }, }, @@ -449,7 +454,7 @@ func TestPostAction(t *testing.T) { postSubpath, err := th.App.CreatePostAsUser(th.Context, &interactivePostSubpath, "", true) require.Nil(t, err) - attachmentsSubpath, ok := postSubpath.GetProp("attachments").([]*model.SlackAttachment) + attachmentsSubpath, ok := postSubpath.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) _, err = th.App.DoPostActionWithCookie(th.Context, postSubpath.Id, attachmentsSubpath[0].Actions[0].Id, th.BasicUser.Id, "", nil) @@ -477,7 +482,7 @@ func TestPostActionProps(t *testing.T) { "has_reactions": true, "is_pinned": false, "props": { - "from_webhook":true, + "from_webhook":"true", "override_username":"new_override_user", "override_icon_url":"new_override_icon", "A":"AA" @@ -496,11 +501,14 @@ func TestPostActionProps(t *testing.T) { HasReactions: false, IsPinned: true, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeSelect, + Name: "action", + DataSource: model.PostActionDataSourceUsers, Integration: &model.PostActionIntegration{ Context: model.StringInterface{ "s": "foo", @@ -508,27 +516,24 @@ func TestPostActionProps(t *testing.T) { }, URL: ts.URL, }, - Name: "action", - Type: "some_type", - DataSource: "some_source", }, }, }, }, - "override_icon_url": "old_override_icon", - "from_webhook": false, - "B": "BB", + model.PostPropsOverrideIconURL: "old_override_icon", + model.PostPropsFromWebhook: "false", + "B": "BB", }, } post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true) require.Nil(t, err) - attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) clientTriggerId, err := th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil) require.Nil(t, err) - assert.True(t, len(clientTriggerId) == 26) + assert.Len(t, clientTriggerId, 26) newPost, nErr := th.App.Srv().Store().Post().GetSingle(th.Context, post.Id, false) require.NoError(t, nErr) @@ -536,10 +541,10 @@ func TestPostActionProps(t *testing.T) { assert.True(t, newPost.IsPinned) assert.False(t, newPost.HasReactions) assert.Nil(t, newPost.GetProp("B")) - assert.Nil(t, newPost.GetProp("override_username")) + assert.Nil(t, newPost.GetProp(model.PostPropsOverrideUsername)) assert.Equal(t, "AA", newPost.GetProp("A")) - assert.Equal(t, "old_override_icon", newPost.GetProp("override_icon_url")) - assert.Equal(t, false, newPost.GetProp("from_webhook")) + assert.Equal(t, "old_override_icon", newPost.GetProp(model.PostPropsOverrideIconURL)) + assert.Equal(t, "false", newPost.GetProp(model.PostPropsFromWebhook)) } func TestSubmitInteractiveDialog(t *testing.T) { @@ -688,16 +693,16 @@ func TestPostActionRelativeURL(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeButton, + Name: "action", Integration: &model.PostActionIntegration{ URL: "/notaplugin/some/path", }, - Name: "action", - Type: "some_type", }, }, }, @@ -707,7 +712,7 @@ func TestPostActionRelativeURL(t *testing.T) { post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true) require.Nil(t, err) - attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) require.NotEmpty(t, attachments[0].Actions) require.NotEmpty(t, attachments[0].Actions[0].Id) @@ -728,16 +733,16 @@ func TestPostActionRelativeURL(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeButton, + Name: "action", Integration: &model.PostActionIntegration{ URL: "/plugins/myplugin/myaction", }, - Name: "action", - Type: "some_type", }, }, }, @@ -747,7 +752,7 @@ func TestPostActionRelativeURL(t *testing.T) { post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true) require.Nil(t, err) - attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) require.NotEmpty(t, attachments[0].Actions) require.NotEmpty(t, attachments[0].Actions[0].Id) @@ -768,16 +773,16 @@ func TestPostActionRelativeURL(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeButton, + Name: "action", Integration: &model.PostActionIntegration{ URL: "/plugins/myplugin/myaction", }, - Name: "action", - Type: "some_type", }, }, }, @@ -787,7 +792,7 @@ func TestPostActionRelativeURL(t *testing.T) { post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true) require.Nil(t, err) - attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) require.NotEmpty(t, attachments[0].Actions) require.NotEmpty(t, attachments[0].Actions[0].Id) @@ -808,16 +813,16 @@ func TestPostActionRelativeURL(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeButton, + Name: "action", Integration: &model.PostActionIntegration{ URL: "//plugins/myplugin///myaction", }, - Name: "action", - Type: "some_type", }, }, }, @@ -827,7 +832,7 @@ func TestPostActionRelativeURL(t *testing.T) { post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true) require.Nil(t, err) - attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) require.NotEmpty(t, attachments[0].Actions) require.NotEmpty(t, attachments[0].Actions[0].Id) @@ -848,16 +853,16 @@ func TestPostActionRelativeURL(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeButton, + Name: "action", Integration: &model.PostActionIntegration{ URL: "plugins/myplugin/myaction", }, - Name: "action", - Type: "some_type", }, }, }, @@ -867,7 +872,7 @@ func TestPostActionRelativeURL(t *testing.T) { post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true) require.Nil(t, err) - attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) require.NotEmpty(t, attachments[0].Actions) require.NotEmpty(t, attachments[0].Actions[0].Id) @@ -925,16 +930,16 @@ func TestPostActionRelativePluginURL(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeButton, + Name: "action", Integration: &model.PostActionIntegration{ URL: "/notaplugin/some/path", }, - Name: "action", - Type: "some_type", }, }, }, @@ -944,7 +949,7 @@ func TestPostActionRelativePluginURL(t *testing.T) { post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true) require.Nil(t, err) - attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) require.NotEmpty(t, attachments[0].Actions) require.NotEmpty(t, attachments[0].Actions[0].Id) @@ -965,16 +970,16 @@ func TestPostActionRelativePluginURL(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeButton, + Name: "action", Integration: &model.PostActionIntegration{ URL: "/plugins/myplugin/myaction", }, - Name: "action", - Type: "some_type", }, }, }, @@ -984,7 +989,7 @@ func TestPostActionRelativePluginURL(t *testing.T) { post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true) require.Nil(t, err) - attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) require.NotEmpty(t, attachments[0].Actions) require.NotEmpty(t, attachments[0].Actions[0].Id) @@ -1005,16 +1010,16 @@ func TestPostActionRelativePluginURL(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeButton, + Name: "action", Integration: &model.PostActionIntegration{ URL: "//plugins/myplugin///myaction", }, - Name: "action", - Type: "some_type", }, }, }, @@ -1024,7 +1029,7 @@ func TestPostActionRelativePluginURL(t *testing.T) { post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true) require.Nil(t, err) - attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) require.NotEmpty(t, attachments[0].Actions) require.NotEmpty(t, attachments[0].Actions[0].Id) @@ -1045,16 +1050,16 @@ func TestPostActionRelativePluginURL(t *testing.T) { PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "hello", Actions: []*model.PostAction{ { + Type: model.PostActionTypeButton, + Name: "action", Integration: &model.PostActionIntegration{ URL: "plugins/myplugin/myaction", }, - Name: "action", - Type: "some_type", }, }, }, @@ -1064,7 +1069,7 @@ func TestPostActionRelativePluginURL(t *testing.T) { post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true) require.Nil(t, err) - attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) require.True(t, ok) require.NotEmpty(t, attachments[0].Actions) require.NotEmpty(t, attachments[0].Actions[0].Id) diff --git a/server/channels/app/mention_parser_standard_test.go b/server/channels/app/mention_parser_standard_test.go index 3d5c83697c..f770dc9513 100644 --- a/server/channels/app/mention_parser_standard_test.go +++ b/server/channels/app/mention_parser_standard_test.go @@ -105,7 +105,7 @@ func TestIsKeywordMultibyte(t *testing.T) { post := &model.Post{ Message: tc.Message, Props: model.StringInterface{ - "attachments": tc.Attachments, + model.PostPropsAttachments: tc.Attachments, }, } diff --git a/server/channels/app/notification.go b/server/channels/app/notification.go index 7475d3afeb..1c4dd0e393 100644 --- a/server/channels/app/notification.go +++ b/server/channels/app/notification.go @@ -209,7 +209,7 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea for _, profile := range profileMap { if (profile.NotifyProps[model.PushNotifyProp] == model.UserNotifyAll || channelMemberNotifyPropsMap[profile.Id][model.PushNotifyProp] == model.ChannelNotifyAll) && - (post.UserId != profile.Id || post.GetProp("from_webhook") == "true") && + (post.UserId != profile.Id || post.GetProp(model.PostPropsFromWebhook) == "true") && !post.IsSystemMessage() && !(a.IsCRTEnabledForUser(c, profile.Id) && post.RootId != "") { allActivityPushUserIds = append(allActivityPushUserIds, profile.Id) @@ -228,7 +228,7 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea var rootMentions *MentionResults if parentPostList != nil { rootPost := parentPostList.Posts[parentPostList.Order[0]] - if rootPost.GetProp("from_webhook") != "true" { + if rootPost.GetProp(model.PostPropsFromWebhook) != "true" { if _, ok := profileMap[rootPost.UserId]; ok { threadParticipants[rootPost.UserId] = true } @@ -358,7 +358,7 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea continue } - if post.GetProp("from_webhook") != "true" && uid == post.UserId { + if post.GetProp(model.PostPropsFromWebhook) != "true" && uid == post.UserId { continue } @@ -1038,7 +1038,7 @@ func (a *App) getExplicitMentionsAndKeywords(c request.CTX, post *model.Post, ch var keywords MentionKeywords if channel.Type == model.ChannelTypeDirect { - isWebhook := post.GetProp("from_webhook") == "true" + isWebhook := post.GetProp(model.PostPropsFromWebhook) == "true" // A bot can post in a DM where it doesn't belong to. // Therefore, we cannot "guess" who is the other user, @@ -1119,7 +1119,7 @@ func (a *App) getExplicitMentionsAndKeywords(c request.CTX, post *model.Post, ch } // Prevent the user from mentioning themselves - if post.GetProp("from_webhook") != "true" { + if post.GetProp(model.PostPropsFromWebhook) != "true" { mentions.removeMention(post.UserId) } } @@ -1639,7 +1639,7 @@ func (n *PostNotification) GetSenderName(userNameFormat string, overridesAllowed } if overridesAllowed && n.Channel.Type != model.ChannelTypeDirect { - if value := n.Post.GetProps()["override_username"]; value != nil && n.Post.GetProp("from_webhook") == "true" { + if value := n.Post.GetProp(model.PostPropsOverrideUsername); value != nil && n.Post.GetProp(model.PostPropsFromWebhook) == "true" { if s, ok := value.(string); ok { return s } diff --git a/server/channels/app/notification_push.go b/server/channels/app/notification_push.go index 53f39dff5e..cd4cfd0d12 100644 --- a/server/channels/app/notification_push.go +++ b/server/channels/app/notification_push.go @@ -668,7 +668,7 @@ func doesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps m } if (notify == model.ChannelNotifyAll) && - (post.UserId != user.Id || post.GetProp("from_webhook") == "true") { + (post.UserId != user.Id || post.GetProp(model.PostPropsFromWebhook) == "true") { return "" } @@ -833,16 +833,16 @@ func (a *App) buildFullPushNotificationMessage(c request.CTX, contentsConfig str } msg.SenderName = senderName - if ou, ok := post.GetProp("override_username").(string); ok && *cfg.ServiceSettings.EnablePostUsernameOverride { + if ou, ok := post.GetProp(model.PostPropsOverrideUsername).(string); ok && *cfg.ServiceSettings.EnablePostUsernameOverride { msg.OverrideUsername = ou msg.SenderName = ou } - if oi, ok := post.GetProp("override_icon_url").(string); ok && *cfg.ServiceSettings.EnablePostIconOverride { + if oi, ok := post.GetProp(model.PostPropsOverrideIconURL).(string); ok && *cfg.ServiceSettings.EnablePostIconOverride { msg.OverrideIconURL = oi } - if fw, ok := post.GetProp("from_webhook").(string); ok { + if fw, ok := post.GetProp(model.PostPropsFromWebhook).(string); ok { msg.FromWebhook = fw } diff --git a/server/channels/app/notification_push_test.go b/server/channels/app/notification_push_test.go index ff19731ff2..182089c8c2 100644 --- a/server/channels/app/notification_push_test.go +++ b/server/channels/app/notification_push_test.go @@ -1617,7 +1617,7 @@ func TestPushNotificationAttachment(t *testing.T) { post := &model.Post{ Message: originalMessage, Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { AuthorName: "testuser", Text: "test attachment", diff --git a/server/channels/app/notification_test.go b/server/channels/app/notification_test.go index 53cdfc8e6d..406c53b773 100644 --- a/server/channels/app/notification_test.go +++ b/server/channels/app/notification_test.go @@ -175,7 +175,7 @@ func TestSendNotifications(t *testing.T) { UserId: user.Id, ChannelId: th.BasicChannel.Id, Message: "a message", - Props: model.StringInterface{"from_webhook": "true", "override_username": "a bot"}, + Props: model.StringInterface{model.PostPropsFromWebhook: "true", model.PostPropsOverrideUsername: "a bot"}, } rootPost, appErr := th.App.CreatePostMissingChannel(th.Context, rootPost, false, true) @@ -1428,7 +1428,7 @@ func TestGetExplicitMentions(t *testing.T) { post := &model.Post{ Message: tc.Message, Props: model.StringInterface{ - "attachments": tc.Attachments, + model.PostPropsAttachments: tc.Attachments, }, } @@ -2208,7 +2208,7 @@ func TestGetMentionsEnabledFields(t *testing.T) { post := &model.Post{ Message: "This is the message", Props: model.StringInterface{ - "attachments": attachments, + model.PostPropsAttachments: attachments, }, } expectedFields := []string{ @@ -2316,22 +2316,22 @@ func TestPostNotificationGetSenderName(t *testing.T) { overriddenPost := &model.Post{ Props: model.StringInterface{ - "override_username": "Overridden", - "from_webhook": "true", + model.PostPropsOverrideUsername: "Overridden", + model.PostPropsFromWebhook: "true", }, } overriddenPost2 := &model.Post{ Props: model.StringInterface{ - "override_username": nil, - "from_webhook": "true", + model.PostPropsOverrideUsername: nil, + model.PostPropsFromWebhook: "true", }, } overriddenPost3 := &model.Post{ Props: model.StringInterface{ - "override_username": 10, - "from_webhook": "true", + model.PostPropsOverrideUsername: 10, + model.PostPropsFromWebhook: "true", }, } @@ -2364,7 +2364,7 @@ func TestPostNotificationGetSenderName(t *testing.T) { "overridden username": { post: overriddenPost, allowOverrides: true, - expected: overriddenPost.GetProp("override_username").(string), + expected: overriddenPost.GetProp(model.PostPropsOverrideUsername).(string), }, "overridden username, direct channel": { channel: &model.Channel{Type: model.ChannelTypeDirect}, @@ -2887,7 +2887,7 @@ func TestReplyPostNotificationsWithCRT(t *testing.T) { UserId: user.Id, ChannelId: th.BasicChannel.Id, Message: "a message", - Props: model.StringInterface{"from_webhook": "true", "override_username": "a bot"}, + Props: model.StringInterface{model.PostPropsFromWebhook: "true", model.PostPropsOverrideUsername: "a bot"}, } rootPost, appErr := th.App.CreatePostMissingChannel(th.Context, rootPost, false, true) diff --git a/server/channels/app/plugin_api.go b/server/channels/app/plugin_api.go index 822579561b..158899e694 100644 --- a/server/channels/app/plugin_api.go +++ b/server/channels/app/plugin_api.go @@ -756,7 +756,7 @@ func (api *PluginAPI) DeleteGroupSyncable(groupID string, syncableID string, syn } func (api *PluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) { - post.AddProp("from_plugin", "true") + post.AddProp(model.PostPropsFromPlugin, "true") post, appErr := api.app.CreatePostMissingChannel(api.ctx, post, true, true) if post != nil { diff --git a/server/channels/app/plugin_api_test.go b/server/channels/app/plugin_api_test.go index e259ff5025..9a543b4f45 100644 --- a/server/channels/app/plugin_api_test.go +++ b/server/channels/app/plugin_api_test.go @@ -1520,7 +1520,7 @@ func TestPluginCreatePostAddsFromPluginProp(t *testing.T) { actualPost, err := api.GetPost(post.Id) require.Nil(t, err) - assert.Equal(t, "true", actualPost.GetProp("from_plugin")) + assert.Equal(t, "true", actualPost.GetProp(model.PostPropsFromPlugin)) } func TestPluginAPIGetConfig(t *testing.T) { diff --git a/server/channels/app/post.go b/server/channels/app/post.go index 229eac960e..de99ca9e1c 100644 --- a/server/channels/app/post.go +++ b/server/channels/app/post.go @@ -297,12 +297,12 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel } // Temporary fix so old plugins don't clobber new fields in SlackAttachment struct, see MM-13088 - if attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment); ok { + if attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment); ok { jsonAttachments, err := json.Marshal(attachments) if err == nil { attachmentsInterface := []any{} err = json.Unmarshal(jsonAttachments, &attachmentsInterface) - post.AddProp("attachments", attachmentsInterface) + post.AddProp(model.PostPropsAttachments, attachmentsInterface) } if err != nil { c.Logger().Warn("Could not convert post attachments to map interface.", mlog.Err(err)) @@ -524,9 +524,9 @@ func (a *App) FillInPostProps(c request.CTX, post *model.Post, channel *model.Ch } if len(channelMentionsProp) > 0 { - post.AddProp("channel_mentions", channelMentionsProp) + post.AddProp(model.PostPropsChannelMentions, channelMentionsProp) } else if post.GetProps() != nil { - post.DelProp("channel_mentions") + post.DelProp(model.PostPropsChannelMentions) } matched := atMentionPattern.MatchString(post.Message) @@ -2077,7 +2077,7 @@ func isCommentMention(user *model.User, post *model.Post, otherPosts map[string] func isPostMention(user *model.User, post *model.Post, keywords MentionKeywords, otherPosts map[string]*model.Post, mentionedByThread map[string]bool, checkForCommentMentions bool) bool { // Prevent the user from mentioning themselves - if post.UserId == user.Id && post.GetProp("from_webhook") != "true" { + if post.UserId == user.Id && post.GetProp(model.PostPropsFromWebhook) != "true" { return false } diff --git a/server/channels/app/post_metadata.go b/server/channels/app/post_metadata.go index 1471e79793..1079ec1bbd 100644 --- a/server/channels/app/post_metadata.go +++ b/server/channels/app/post_metadata.go @@ -38,8 +38,6 @@ type linkMetadataCache struct { const MaxMetadataImageSize = MaxOpenGraphResponseSize -const UnsafeLinksPostProp = "unsafe_links" - func (s *Server) initPostMetadata() { // Dump any cached links if the proxy settings have changed so image URLs can be updated s.platform.AddConfigListener(func(before, after *model.Config) { @@ -183,7 +181,7 @@ func (a *App) getEmbedsAndImages(c request.CTX, post *model.Post, isNewPost bool // Embeds and image dimensions firstLink, images := a.getFirstLinkAndImages(c, post.Message) - if unsafeLinksProp := post.GetProp(UnsafeLinksPostProp); unsafeLinksProp != nil { + if unsafeLinksProp := post.GetProp(model.PostPropsUnsafeLinks); unsafeLinksProp != nil { if prop, ok := unsafeLinksProp.(string); ok && prop == "true" { images = []string{} if !looksLikeAPermalink(firstLink, *a.Config().ServiceSettings.SiteURL) { @@ -302,7 +300,7 @@ func (a *App) getEmojisAndReactionsForPost(c request.CTX, post *model.Post) ([]* } func (a *App) getEmbedForPost(c request.CTX, post *model.Post, firstLink string, isNewPost bool) (*model.PostEmbed, error) { - if _, ok := post.GetProps()["attachments"]; ok { + if _, ok := post.GetProps()[model.PostPropsAttachments]; ok { return &model.PostEmbed{ Type: model.PostEmbedMessageAttachment, }, nil diff --git a/server/channels/app/post_metadata_test.go b/server/channels/app/post_metadata_test.go index 0cd0b5a66c..081bd24280 100644 --- a/server/channels/app/post_metadata_test.go +++ b/server/channels/app/post_metadata_test.go @@ -223,7 +223,7 @@ func TestPreparePostForClient(t *testing.T) { ChannelId: th.BasicChannel.Id, Message: ":" + emoji.Name + ": :taco:", Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: ":" + emoji.Name + ":", }, @@ -267,7 +267,7 @@ func TestPreparePostForClient(t *testing.T) { ChannelId: th.BasicChannel.Id, Message: ":" + emoji3.Name + ": :taco:", Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: ":" + emoji4.Name + ":", }, @@ -531,7 +531,7 @@ func TestPreparePostForClient(t *testing.T) { ChannelId: th.BasicChannel.Id, Message: `Bla bla bla: ` + fmt.Sprintf(tc.link, noAccessServer.URL), } - prepost.AddProp(UnsafeLinksPostProp, "true") + prepost.AddProp(model.PostPropsUnsafeLinks, "true") post, err := th.App.CreatePost(th.Context, prepost, th.BasicChannel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, err) @@ -569,7 +569,7 @@ func TestPreparePostForClient(t *testing.T) { UserId: th.BasicUser.Id, ChannelId: th.BasicChannel.Id, Props: map[string]any{ - "attachments": []any{ + model.PostPropsAttachments: []any{ map[string]any{ "text": "![icon](" + server.URL + "/test-image1.png)", }, @@ -1039,7 +1039,7 @@ func TestGetEmbedForPost(t *testing.T) { t.Run("should return a message attachment when the post has one", func(t *testing.T) { embed, err := th.App.getEmbedForPost(th.Context, &model.Post{ Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "test", }, @@ -1117,7 +1117,7 @@ func TestGetEmbedForPost(t *testing.T) { t.Run("should return an embedded message attachment", func(t *testing.T) { embed, err := th.App.getEmbedForPost(th.Context, &model.Post{ Props: model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "test", }, @@ -1532,7 +1532,7 @@ func TestGetEmojiNamesForPost(t *testing.T) { Post: &model.Post{ Message: "this is a post", Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: ":emoji1:", Pretext: ":emoji2:", @@ -1560,7 +1560,7 @@ func TestGetEmojiNamesForPost(t *testing.T) { Post: &model.Post{ Message: "this is :emoji1", Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: ":emoji2:", Pretext: ":emoji2:", @@ -1616,7 +1616,7 @@ func TestGetCustomEmojisForPost(t *testing.T) { post := &model.Post{ Message: ":" + emojis[1].Name + ":", Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Pretext: ":" + emojis[2].Name + ":", Text: ":" + emojis[3].Name + ":", @@ -1642,7 +1642,7 @@ func TestGetCustomEmojisForPost(t *testing.T) { post := &model.Post{ Message: ":secret: :" + emojis[0].Name + ":", Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: ":imaginary:", }, @@ -1838,7 +1838,7 @@ func TestGetImagesInMessageAttachments(t *testing.T) { Name: "empty attachments", Post: &model.Post{ Props: map[string]any{ - "attachments": []*model.SlackAttachment{}, + model.PostPropsAttachments: []*model.SlackAttachment{}, }, }, Expected: []string{}, @@ -1847,7 +1847,7 @@ func TestGetImagesInMessageAttachments(t *testing.T) { Name: "attachment with no fields that can contain images", Post: &model.Post{ Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Title: "This is the title", }, @@ -1860,7 +1860,7 @@ func TestGetImagesInMessageAttachments(t *testing.T) { Name: "images in text", Post: &model.Post{ Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "![logo](https://example.com/logo) and ![icon](https://example.com/icon)", }, @@ -1873,7 +1873,7 @@ func TestGetImagesInMessageAttachments(t *testing.T) { Name: "images in pretext", Post: &model.Post{ Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Pretext: "![logo](https://example.com/logo1) and ![icon](https://example.com/icon1)", }, @@ -1886,7 +1886,7 @@ func TestGetImagesInMessageAttachments(t *testing.T) { Name: "images in fields", Post: &model.Post{ Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Fields: []*model.SlackAttachmentField{ { @@ -1903,7 +1903,7 @@ func TestGetImagesInMessageAttachments(t *testing.T) { Name: "image in author_icon", Post: &model.Post{ Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { AuthorIcon: "https://example.com/icon2", }, @@ -1916,7 +1916,7 @@ func TestGetImagesInMessageAttachments(t *testing.T) { Name: "image in image_url", Post: &model.Post{ Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { ImageURL: "https://example.com/image", }, @@ -1929,7 +1929,7 @@ func TestGetImagesInMessageAttachments(t *testing.T) { Name: "image in thumb_url", Post: &model.Post{ Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { ThumbURL: "https://example.com/image", }, @@ -1942,7 +1942,7 @@ func TestGetImagesInMessageAttachments(t *testing.T) { Name: "image in footer_icon", Post: &model.Post{ Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { FooterIcon: "https://example.com/image", }, @@ -1955,7 +1955,7 @@ func TestGetImagesInMessageAttachments(t *testing.T) { Name: "images in multiple fields", Post: &model.Post{ Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Fields: []*model.SlackAttachmentField{ { @@ -1975,7 +1975,7 @@ func TestGetImagesInMessageAttachments(t *testing.T) { Name: "non-string field", Post: &model.Post{ Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Fields: []*model.SlackAttachmentField{ { @@ -1992,7 +1992,7 @@ func TestGetImagesInMessageAttachments(t *testing.T) { Name: "images in multiple locations", Post: &model.Post{ Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "![text](https://example.com/text)", Pretext: "![pretext](https://example.com/pretext)", @@ -2014,7 +2014,7 @@ func TestGetImagesInMessageAttachments(t *testing.T) { Name: "multiple attachments", Post: &model.Post{ Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "![logo](https://example.com/logo)", }, diff --git a/server/channels/app/post_test.go b/server/channels/app/post_test.go index 4cb9b9541f..4fef95eeb4 100644 --- a/server/channels/app/post_test.go +++ b/server/channels/app/post_test.go @@ -588,7 +588,7 @@ func TestPostChannelMentions(t *testing.T) { "display_name": "Mention Test", "team_name": th.BasicTeam.Name, }, - }, post.GetProp("channel_mentions")) + }, post.GetProp(model.PostPropsChannelMentions)) post.Message = fmt.Sprintf("goodbye, ~%v!", channelToMention2.Name) result, err := th.App.UpdatePost(th.Context, post, nil) @@ -598,12 +598,12 @@ func TestPostChannelMentions(t *testing.T) { "display_name": "Mention Test2", "team_name": th.BasicTeam.Name, }, - }, result.GetProp("channel_mentions")) + }, result.GetProp(model.PostPropsChannelMentions)) result.Message = "no more mentions!" result, err = th.App.UpdatePost(th.Context, result, nil) require.Nil(t, err) - assert.Nil(t, result.GetProp("channel_mentions")) + assert.Nil(t, result.GetProp(model.PostPropsChannelMentions)) } func TestImageProxy(t *testing.T) { @@ -1282,7 +1282,7 @@ func TestCreatePostAsUser(t *testing.T) { Message: "test", UserId: th.BasicUser.Id, } - post.AddProp("from_webhook", "true") + post.AddProp(model.PostPropsFromWebhook, "true") channelMemberBefore, err := th.App.Srv().Store().Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id) require.NoError(t, err) @@ -2502,7 +2502,7 @@ func TestCountMentionsFromPost(t *testing.T) { ChannelId: channel.Id, Message: fmt.Sprintf("@%s", user2.Username), Props: map[string]any{ - "from_webhook": "true", + model.PostPropsFromWebhook: "true", }, }, channel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, err) diff --git a/server/channels/app/slashcommands/command_test.go b/server/channels/app/slashcommands/command_test.go index d9dbd0896a..91860229bf 100644 --- a/server/channels/app/slashcommands/command_test.go +++ b/server/channels/app/slashcommands/command_test.go @@ -165,15 +165,15 @@ func TestHandleCommandResponsePost(t *testing.T) { assert.Equal(t, resp.Type, post.Type) assert.Equal(t, resp.Props, post.GetProps()) assert.Equal(t, resp.Text, post.Message) - assert.Nil(t, post.GetProp("override_icon_url")) - assert.Nil(t, post.GetProp("override_username")) - assert.Nil(t, post.GetProp("from_webhook")) + assert.Nil(t, post.GetProp(model.PostPropsOverrideIconURL)) + assert.Nil(t, post.GetProp(model.PostPropsOverrideUsername)) + assert.Nil(t, post.GetProp(model.PostPropsFromWebhook)) // Command is not built in, so it is a bot command. builtIn = false post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn) assert.Nil(t, err) - assert.Equal(t, "true", post.GetProp("from_webhook")) + assert.Equal(t, "true", post.GetProp(model.PostPropsFromWebhook)) builtIn = true @@ -195,23 +195,23 @@ func TestHandleCommandResponsePost(t *testing.T) { post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn) assert.Nil(t, err) - assert.Nil(t, post.GetProp("override_username")) + assert.Nil(t, post.GetProp(model.PostPropsOverrideUsername)) *th.App.Config().ServiceSettings.EnablePostUsernameOverride = true // Override username config is turned on. Override username through command property. post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn) assert.Nil(t, err) - assert.Equal(t, command.Username, post.GetProp("override_username")) - assert.Equal(t, "true", post.GetProp("from_webhook")) + assert.Equal(t, command.Username, post.GetProp(model.PostPropsOverrideUsername)) + assert.Equal(t, "true", post.GetProp(model.PostPropsFromWebhook)) command.Username = "" // Override username through response property. post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn) assert.Nil(t, err) - assert.Equal(t, resp.Username, post.GetProp("override_username")) - assert.Equal(t, "true", post.GetProp("from_webhook")) + assert.Equal(t, resp.Username, post.GetProp(model.PostPropsOverrideUsername)) + assert.Equal(t, "true", post.GetProp(model.PostPropsFromWebhook)) *th.App.Config().ServiceSettings.EnablePostUsernameOverride = false @@ -222,23 +222,23 @@ func TestHandleCommandResponsePost(t *testing.T) { post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn) assert.Nil(t, err) - assert.Nil(t, post.GetProp("override_icon_url")) + assert.Nil(t, post.GetProp(model.PostPropsOverrideIconURL)) *th.App.Config().ServiceSettings.EnablePostIconOverride = true // Override icon url config is turned on. Override icon url through command property. post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn) assert.Nil(t, err) - assert.Equal(t, command.IconURL, post.GetProp("override_icon_url")) - assert.Equal(t, "true", post.GetProp("from_webhook")) + assert.Equal(t, command.IconURL, post.GetProp(model.PostPropsOverrideIconURL)) + assert.Equal(t, "true", post.GetProp(model.PostPropsFromWebhook)) command.IconURL = "" // Override icon url through response property. post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn) assert.Nil(t, err) - assert.Equal(t, resp.IconURL, post.GetProp("override_icon_url")) - assert.Equal(t, "true", post.GetProp("from_webhook")) + assert.Equal(t, resp.IconURL, post.GetProp(model.PostPropsOverrideIconURL)) + assert.Equal(t, "true", post.GetProp(model.PostPropsFromWebhook)) // Test Slack text conversion. resp.Text = "" @@ -246,7 +246,7 @@ func TestHandleCommandResponsePost(t *testing.T) { post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn) assert.Nil(t, err) assert.Equal(t, "@channel", post.Message) - assert.Equal(t, "true", post.GetProp("from_webhook")) + assert.Equal(t, "true", post.GetProp(model.PostPropsFromWebhook)) // Test Slack attachments text conversion. resp.Attachments = []*model.SlackAttachment{ @@ -261,7 +261,7 @@ func TestHandleCommandResponsePost(t *testing.T) { if assert.Len(t, post.Attachments(), 1) { assert.Equal(t, "@here", post.Attachments()[0].Text) } - assert.Equal(t, "true", post.GetProp("from_webhook")) + assert.Equal(t, "true", post.GetProp(model.PostPropsFromWebhook)) channel = th.createPrivateChannel(th.BasicTeam) resp.ChannelId = channel.Id diff --git a/server/channels/app/webhook.go b/server/channels/app/webhook.go index d6d3039beb..09e265e961 100644 --- a/server/channels/app/webhook.go +++ b/server/channels/app/webhook.go @@ -163,7 +163,7 @@ func (a *App) TriggerWebhook(c request.CTX, payload *model.OutgoingWebhookPayloa if len(webhookResp.Props) == 0 { webhookResp.Props = make(model.StringInterface) } - webhookResp.Props["webhook_display_name"] = hook.DisplayName + webhookResp.Props[model.PostPropsWebhookDisplayName] = hook.DisplayName text := "" if webhookResp.Text != nil { @@ -172,7 +172,7 @@ func (a *App) TriggerWebhook(c request.CTX, payload *model.OutgoingWebhookPayloa webhookResp.Attachments = a.ProcessSlackAttachments(webhookResp.Attachments) // attachments is in here for slack compatibility if len(webhookResp.Attachments) > 0 { - webhookResp.Props["attachments"] = webhookResp.Attachments + webhookResp.Props[model.PostPropsAttachments] = webhookResp.Attachments } if *a.Config().ServiceSettings.EnablePostUsernameOverride && hook.Username != "" && webhookResp.Username == "" { webhookResp.Username = hook.Username @@ -232,7 +232,7 @@ func splitWebhookPost(post *model.Post, maxPostSize int) ([]*model.Post, *model. base.Message = "" base.SetProps(make(map[string]any)) for k, v := range post.GetProps() { - if k != "attachments" { + if k != model.PostPropsAttachments { base.AddProp(k, v) } } @@ -259,7 +259,7 @@ func splitWebhookPost(post *model.Post, maxPostSize int) ([]*model.Post, *model. split.Message = remainingText splits = append(splits, split) - attachments, _ := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, _ := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) for _, attachment := range attachments { newAttachment := *attachment for { @@ -268,8 +268,8 @@ func splitWebhookPost(post *model.Post, maxPostSize int) ([]*model.Post, *model. for k, v := range lastSplit.GetProps() { newProps[k] = v } - origAttachments, _ := newProps["attachments"].([]*model.SlackAttachment) - newProps["attachments"] = append(origAttachments, &newAttachment) + origAttachments, _ := newProps[model.PostPropsAttachments].([]*model.SlackAttachment) + newProps[model.PostPropsAttachments] = append(origAttachments, &newAttachment) newPropsString := model.StringInterfaceToJSON(newProps) runeCount := utf8.RuneCountInString(newPropsString) @@ -310,7 +310,7 @@ func (a *App) CreateWebhookPost(c request.CTX, userID string, channel *model.Cha text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})") post := &model.Post{UserId: userID, ChannelId: channel.Id, Message: text, Type: postType, RootId: postRootId} - post.AddProp("from_webhook", "true") + post.AddProp(model.PostPropsFromWebhook, "true") if priority != nil { if priority.Priority == nil { @@ -333,28 +333,33 @@ func (a *App) CreateWebhookPost(c request.CTX, userID string, channel *model.Cha if *a.Config().ServiceSettings.EnablePostUsernameOverride { if overrideUsername != "" { - post.AddProp("override_username", overrideUsername) + post.AddProp(model.PostPropsOverrideUsername, overrideUsername) } else { - post.AddProp("override_username", model.DefaultWebhookUsername) + post.AddProp(model.PostPropsOverrideUsername, model.DefaultWebhookUsername) } } if *a.Config().ServiceSettings.EnablePostIconOverride { if overrideIconURL != "" { - post.AddProp("override_icon_url", overrideIconURL) + post.AddProp(model.PostPropsOverrideIconURL, overrideIconURL) } if overrideIconEmoji != "" { - post.AddProp("override_icon_emoji", overrideIconEmoji) + post.AddProp(model.PostPropsOverrideIconURL, overrideIconEmoji) } } if len(props) > 0 { for key, val := range props { - if key == "attachments" { + switch key { + case model.PostPropsAttachments: if attachments, success := val.([]*model.SlackAttachment); success { model.ParseSlackAttachment(post, attachments) } - } else if key != "override_icon_url" && key != "override_username" && key != "from_webhook" { + case model.PostPropsOverrideIconURL, + model.PostPropsOverrideUsername, + model.PostPropsFromWebhook: + // Do nothing + default: post.AddProp(key, val) } } @@ -765,13 +770,13 @@ func (a *App) HandleIncomingWebhook(c request.CTX, hookID string, req *model.Inc req.Props = make(model.StringInterface) } - req.Props["webhook_display_name"] = hook.DisplayName + req.Props[model.PostPropsWebhookDisplayName] = hook.DisplayName text = a.ProcessSlackText(text) req.Attachments = a.ProcessSlackAttachments(req.Attachments) // attachments is in here for slack compatibility if len(req.Attachments) > 0 { - req.Props["attachments"] = req.Attachments + req.Props[model.PostPropsAttachments] = req.Attachments webhookType = model.PostTypeSlackAttachment } diff --git a/server/channels/app/webhook_test.go b/server/channels/app/webhook_test.go index a1e0f7f385..0e6c0b9bc4 100644 --- a/server/channels/app/webhook_test.go +++ b/server/channels/app/webhook_test.go @@ -298,44 +298,44 @@ func TestCreateWebhookPost(t *testing.T) { post, err := th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, "foo", "user", "http://iconurl", "", model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "text", }, }, - "webhook_display_name": hook.DisplayName, + model.PostPropsWebhookDisplayName: hook.DisplayName, }, model.PostTypeSlackAttachment, "", nil) require.Nil(t, err) - assert.Contains(t, post.GetProps(), "from_webhook", "missing from_webhook prop") - assert.Contains(t, post.GetProps(), "attachments", "missing attachments prop") - assert.Contains(t, post.GetProps(), "webhook_display_name", "missing webhook_display_name prop") + assert.Contains(t, post.GetProps(), model.PostPropsFromWebhook, "missing from_webhook prop") + assert.Contains(t, post.GetProps(), model.PostPropsAttachments, "missing attachments prop") + assert.Contains(t, post.GetProps(), model.PostPropsWebhookDisplayName, "missing webhook_display_name prop") _, err = th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, "foo", "user", "http://iconurl", "", nil, model.PostTypeSystemGeneric, "", nil) require.NotNil(t, err, "Should have failed - bad post type") expectedText := "`<>|<>|`" post, err = th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", "", model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "text", }, }, - "webhook_display_name": hook.DisplayName, + model.PostPropsWebhookDisplayName: hook.DisplayName, }, model.PostTypeSlackAttachment, "", nil) require.Nil(t, err) assert.Equal(t, expectedText, post.Message) expectedText = "< | \n|\n>" post, err = th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", "", model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "text", }, }, - "webhook_display_name": hook.DisplayName, + model.PostPropsWebhookDisplayName: hook.DisplayName, }, model.PostTypeSlackAttachment, "", nil) require.Nil(t, err) assert.Equal(t, expectedText, post.Message) @@ -358,12 +358,12 @@ Date: Thu Mar 1 19:46:48 2018 +0300 test | 3 +++ 1 file changed, 3 insertions(+)` post, err = th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", "", model.StringInterface{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: "text", }, }, - "webhook_display_name": hook.DisplayName, + model.PostPropsWebhookDisplayName: hook.DisplayName, }, model.PostTypeSlackAttachment, "", nil) require.Nil(t, err) assert.Equal(t, expectedText, post.Message) @@ -415,7 +415,7 @@ func TestCreateWebhookPostWithPriority(t *testing.T) { for _, conditions := range testConditions { post, err := th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, "foo @"+th.BasicUser.Username, "user", "http://iconurl", "", - model.StringInterface{"webhook_display_name": hook.DisplayName}, + model.StringInterface{model.PostPropsWebhookDisplayName: hook.DisplayName}, model.PostTypeSlackAttachment, "", &conditions, @@ -424,7 +424,7 @@ func TestCreateWebhookPostWithPriority(t *testing.T) { require.Nil(t, err) assert.Equal(t, post.Message, "foo @"+th.BasicUser.Username) - assert.Contains(t, post.GetProps(), "from_webhook", "missing from_webhook prop") + assert.Contains(t, post.GetProps(), model.PostPropsFromWebhook, "missing from_webhook prop") assert.Equal(t, *conditions.Priority, *post.GetPriority().Priority) assert.Equal(t, *conditions.RequestedAck, *post.GetPriority().RequestedAck) assert.Equal(t, *conditions.PersistentNotifications, *post.GetPriority().PersistentNotifications) @@ -488,7 +488,7 @@ func TestSplitWebhookPost(t *testing.T) { Post: &model.Post{ Message: strings.Repeat("本", maxPostSize*3/2), Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: strings.Repeat("本", 1000), }, @@ -508,7 +508,7 @@ func TestSplitWebhookPost(t *testing.T) { { Message: strings.Repeat("本", maxPostSize/2), Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: strings.Repeat("本", 1000), }, @@ -520,7 +520,7 @@ func TestSplitWebhookPost(t *testing.T) { }, { Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Text: strings.Repeat("本", model.PostPropsMaxUserRunes-1000), }, @@ -549,7 +549,7 @@ func TestSplitWebhookPost(t *testing.T) { for i, split := range splits { if i < len(tc.Expected) { assert.Equal(t, tc.Expected[i].Message, split.Message) - assert.Equal(t, tc.Expected[i].GetProp("attachments"), split.GetProp("attachments")) + assert.Equal(t, tc.Expected[i].GetProp(model.PostPropsAttachments), split.GetProp(model.PostPropsAttachments)) } } }) @@ -566,7 +566,7 @@ func makePost(message int, attachments []int) *model.Post { } sa = append(sa, attach) } - props = map[string]any{"attachments": sa} + props = map[string]any{model.PostPropsAttachments: sa} } post := &model.Post{ Message: strings.Repeat("那", message), @@ -628,7 +628,7 @@ func TestSplitWebhookPostAttachments(t *testing.T) { for i, split := range splits { if i < len(tc.expected) { assert.Equal(t, tc.expected[i].Message, split.Message, i) - assert.Equal(t, tc.expected[i].GetProp("attachments"), split.GetProp("attachments"), i) + assert.Equal(t, tc.expected[i].GetProp(model.PostPropsAttachments), split.GetProp(model.PostPropsAttachments), i) } } }) @@ -786,17 +786,17 @@ func TestTriggerOutGoingWebhookWithUsernameAndIconURL(t *testing.T) { select { case webhookPost := <-createdPost: assert.Equal(t, webhookPost.Message, "sample response text from test server") - assert.Equal(t, webhookPost.GetProp("from_webhook"), "true") + assert.Equal(t, webhookPost.GetProp(model.PostPropsFromWebhook), "true") if testCase.ExpectedIconURL != "" { - assert.Equal(t, webhookPost.GetProp("override_icon_url"), testCase.ExpectedIconURL) + assert.Equal(t, webhookPost.GetProp(model.PostPropsOverrideIconURL), testCase.ExpectedIconURL) } else { - assert.Nil(t, webhookPost.GetProp("override_icon_url")) + assert.Nil(t, webhookPost.GetProp(model.PostPropsOverrideIconURL)) } if testCase.ExpectedUsername != "" { - assert.Equal(t, webhookPost.GetProp("override_username"), testCase.ExpectedUsername) + assert.Equal(t, webhookPost.GetProp(model.PostPropsOverrideUsername), testCase.ExpectedUsername) } else { - assert.Nil(t, webhookPost.GetProp("override_username")) + assert.Nil(t, webhookPost.GetProp(model.PostPropsOverrideUsername)) } case <-time.After(5 * time.Second): require.Fail(t, "Timeout, webhook response not created as post") diff --git a/server/channels/store/retrylayer/retrylayer.go b/server/channels/store/retrylayer/retrylayer.go index a59bbea5e8..de877740fa 100644 --- a/server/channels/store/retrylayer/retrylayer.go +++ b/server/channels/store/retrylayer/retrylayer.go @@ -8067,11 +8067,11 @@ func (s *RetryLayerPostStore) Overwrite(rctx request.CTX, post *model.Post) (*mo } -func (s *RetryLayerPostStore) OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, error) { +func (s *RetryLayerPostStore) OverwriteMultiple(rctx request.CTX, posts []*model.Post) ([]*model.Post, int, error) { tries := 0 for { - result, resultVar1, err := s.PostStore.OverwriteMultiple(posts) + result, resultVar1, err := s.PostStore.OverwriteMultiple(rctx, posts) if err == nil { return result, resultVar1, nil } @@ -8235,11 +8235,11 @@ func (s *RetryLayerPostStore) Save(rctx request.CTX, post *model.Post) (*model.P } -func (s *RetryLayerPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, error) { +func (s *RetryLayerPostStore) SaveMultiple(rctx request.CTX, posts []*model.Post) ([]*model.Post, int, error) { tries := 0 for { - result, resultVar1, err := s.PostStore.SaveMultiple(posts) + result, resultVar1, err := s.PostStore.SaveMultiple(rctx, posts) if err == nil { return result, resultVar1, nil } diff --git a/server/channels/store/sqlstore/post_store.go b/server/channels/store/sqlstore/post_store.go index 69f25044b5..c5fbaede5f 100644 --- a/server/channels/store/sqlstore/post_store.go +++ b/server/channels/store/sqlstore/post_store.go @@ -147,7 +147,7 @@ func newSqlPostStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) s } } -func (s *SqlPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, error) { +func (s *SqlPostStore) SaveMultiple(rctx request.CTX, posts []*model.Post) ([]*model.Post, int, error) { channelNewPosts := make(map[string]int) channelNewRootPosts := make(map[string]int) maxDateNewPosts := make(map[string]int64) @@ -160,9 +160,11 @@ func (s *SqlPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, er } post.PreSave() maxPostSize := s.GetMaxPostSize() + if err := post.IsValid(maxPostSize); err != nil { return nil, idx, err } + post.ValidateProps(rctx.Logger()) if currentChannelCount, ok := channelNewPosts[post.ChannelId]; !ok { if post.IsJoinLeaveMessage() { @@ -293,7 +295,7 @@ func (s *SqlPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, er } func (s *SqlPostStore) Save(rctx request.CTX, post *model.Post) (*model.Post, error) { - posts, _, err := s.SaveMultiple([]*model.Post{post}) + posts, _, err := s.SaveMultiple(rctx, []*model.Post{post}) if err != nil { return nil, err } @@ -356,6 +358,7 @@ func (s *SqlPostStore) Update(rctx request.CTX, newPost *model.Post, oldPost *mo if err := newPost.IsValid(maxPostSize); err != nil { return nil, err } + newPost.ValidateProps(rctx.Logger()) if _, err := s.GetMaster().NamedExec(`UPDATE Posts SET CreateAt=:CreateAt, @@ -409,14 +412,16 @@ func (s *SqlPostStore) Update(rctx request.CTX, newPost *model.Post, oldPost *mo return newPost, nil } -func (s *SqlPostStore) OverwriteMultiple(posts []*model.Post) (_ []*model.Post, _ int, err error) { +func (s *SqlPostStore) OverwriteMultiple(rctx request.CTX, posts []*model.Post) (_ []*model.Post, _ int, err error) { updateAt := model.GetMillis() maxPostSize := s.GetMaxPostSize() for idx, post := range posts { post.UpdateAt = updateAt + if appErr := post.IsValid(maxPostSize); appErr != nil { return nil, idx, appErr } + post.ValidateProps(rctx.Logger()) } tx, err := s.GetMaster().Beginx() @@ -464,7 +469,7 @@ func (s *SqlPostStore) OverwriteMultiple(posts []*model.Post) (_ []*model.Post, } func (s *SqlPostStore) Overwrite(rctx request.CTX, post *model.Post) (*model.Post, error) { - posts, _, err := s.OverwriteMultiple([]*model.Post{post}) + posts, _, err := s.OverwriteMultiple(rctx, []*model.Post{post}) if err != nil { return nil, err } diff --git a/server/channels/store/store.go b/server/channels/store/store.go index 1b198488b0..5f7ebae115 100644 --- a/server/channels/store/store.go +++ b/server/channels/store/store.go @@ -364,7 +364,7 @@ type ThreadStore interface { } type PostStore interface { - SaveMultiple(posts []*model.Post) ([]*model.Post, int, error) + SaveMultiple(rctx request.CTX, posts []*model.Post) ([]*model.Post, int, error) Save(rctx request.CTX, post *model.Post) (*model.Post, error) Update(rctx request.CTX, newPost *model.Post, oldPost *model.Post) (*model.Post, error) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) @@ -394,7 +394,7 @@ type PostStore interface { InvalidateLastPostTimeCache(channelID string) GetPostsCreatedAt(channelID string, timestamp int64) ([]*model.Post, error) Overwrite(rctx request.CTX, post *model.Post) (*model.Post, error) - OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, error) + OverwriteMultiple(rctx request.CTX, posts []*model.Post) ([]*model.Post, int, error) GetPostsByIds(postIds []string) ([]*model.Post, error) GetEditHistoryForPost(postID string) ([]*model.Post, error) GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error) diff --git a/server/channels/store/storetest/mocks/PostStore.go b/server/channels/store/storetest/mocks/PostStore.go index bc2d016885..ce8a439108 100644 --- a/server/channels/store/storetest/mocks/PostStore.go +++ b/server/channels/store/storetest/mocks/PostStore.go @@ -1037,9 +1037,9 @@ func (_m *PostStore) Overwrite(rctx request.CTX, post *model.Post) (*model.Post, return r0, r1 } -// OverwriteMultiple provides a mock function with given fields: posts -func (_m *PostStore) OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, error) { - ret := _m.Called(posts) +// OverwriteMultiple provides a mock function with given fields: rctx, posts +func (_m *PostStore) OverwriteMultiple(rctx request.CTX, posts []*model.Post) ([]*model.Post, int, error) { + ret := _m.Called(rctx, posts) if len(ret) == 0 { panic("no return value specified for OverwriteMultiple") @@ -1048,25 +1048,25 @@ func (_m *PostStore) OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, var r0 []*model.Post var r1 int var r2 error - if rf, ok := ret.Get(0).(func([]*model.Post) ([]*model.Post, int, error)); ok { - return rf(posts) + if rf, ok := ret.Get(0).(func(request.CTX, []*model.Post) ([]*model.Post, int, error)); ok { + return rf(rctx, posts) } - if rf, ok := ret.Get(0).(func([]*model.Post) []*model.Post); ok { - r0 = rf(posts) + if rf, ok := ret.Get(0).(func(request.CTX, []*model.Post) []*model.Post); ok { + r0 = rf(rctx, posts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.Post) } } - if rf, ok := ret.Get(1).(func([]*model.Post) int); ok { - r1 = rf(posts) + if rf, ok := ret.Get(1).(func(request.CTX, []*model.Post) int); ok { + r1 = rf(rctx, posts) } else { r1 = ret.Get(1).(int) } - if rf, ok := ret.Get(2).(func([]*model.Post) error); ok { - r2 = rf(posts) + if rf, ok := ret.Get(2).(func(request.CTX, []*model.Post) error); ok { + r2 = rf(rctx, posts) } else { r2 = ret.Error(2) } @@ -1239,9 +1239,9 @@ func (_m *PostStore) Save(rctx request.CTX, post *model.Post) (*model.Post, erro return r0, r1 } -// SaveMultiple provides a mock function with given fields: posts -func (_m *PostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, error) { - ret := _m.Called(posts) +// SaveMultiple provides a mock function with given fields: rctx, posts +func (_m *PostStore) SaveMultiple(rctx request.CTX, posts []*model.Post) ([]*model.Post, int, error) { + ret := _m.Called(rctx, posts) if len(ret) == 0 { panic("no return value specified for SaveMultiple") @@ -1250,25 +1250,25 @@ func (_m *PostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, erro var r0 []*model.Post var r1 int var r2 error - if rf, ok := ret.Get(0).(func([]*model.Post) ([]*model.Post, int, error)); ok { - return rf(posts) + if rf, ok := ret.Get(0).(func(request.CTX, []*model.Post) ([]*model.Post, int, error)); ok { + return rf(rctx, posts) } - if rf, ok := ret.Get(0).(func([]*model.Post) []*model.Post); ok { - r0 = rf(posts) + if rf, ok := ret.Get(0).(func(request.CTX, []*model.Post) []*model.Post); ok { + r0 = rf(rctx, posts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.Post) } } - if rf, ok := ret.Get(1).(func([]*model.Post) int); ok { - r1 = rf(posts) + if rf, ok := ret.Get(1).(func(request.CTX, []*model.Post) int); ok { + r1 = rf(rctx, posts) } else { r1 = ret.Get(1).(int) } - if rf, ok := ret.Get(2).(func([]*model.Post) error); ok { - r2 = rf(posts) + if rf, ok := ret.Get(2).(func(request.CTX, []*model.Post) error); ok { + r2 = rf(rctx, posts) } else { r2 = ret.Error(2) } diff --git a/server/channels/store/storetest/post_acknowledgements_store.go b/server/channels/store/storetest/post_acknowledgements_store.go index dc51735c24..c94fffef1e 100644 --- a/server/channels/store/storetest/post_acknowledgements_store.go +++ b/server/channels/store/storetest/post_acknowledgements_store.go @@ -140,7 +140,7 @@ func testPostAcknowledgementsStoreGetForPosts(t *testing.T, rctx request.CTX, ss PersistentNotifications: model.NewPointer(false), }, } - _, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2}) + _, errIdx, err := ss.Post().SaveMultiple(rctx, []*model.Post{&p1, &p2}) require.NoError(t, err) require.Equal(t, -1, errIdx) diff --git a/server/channels/store/storetest/post_persistent_notification_store.go b/server/channels/store/storetest/post_persistent_notification_store.go index 6c1cced5a8..01acfcea47 100644 --- a/server/channels/store/storetest/post_persistent_notification_store.go +++ b/server/channels/store/storetest/post_persistent_notification_store.go @@ -81,7 +81,7 @@ func testPostPersistentNotificationStoreGet(t *testing.T, rctx request.CTX, ss s }, } - _, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2, &p3, &p4, &p5}) + _, errIdx, err := ss.Post().SaveMultiple(rctx, []*model.Post{&p1, &p2, &p3, &p4, &p5}) require.NoError(t, err) require.Equal(t, -1, errIdx) @@ -156,7 +156,7 @@ func testPostPersistentNotificationStoreUpdateLastSentAt(t *testing.T, rctx requ }, } - _, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1}) + _, errIdx, err := ss.Post().SaveMultiple(rctx, []*model.Post{&p1}) require.NoError(t, err) require.Equal(t, -1, errIdx) @@ -235,7 +235,7 @@ func testPostPersistentNotificationStoreDelete(t *testing.T, rctx request.CTX, s }, } - _, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2, &p3}) + _, errIdx, err := ss.Post().SaveMultiple(rctx, []*model.Post{&p1, &p2, &p3}) require.NoError(t, err) require.Equal(t, -1, errIdx) @@ -321,7 +321,7 @@ func testPostPersistentNotificationStoreDelete(t *testing.T, rctx request.CTX, s }, } - _, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2, &p3, &p4, &p5}) + _, errIdx, err := ss.Post().SaveMultiple(rctx, []*model.Post{&p1, &p2, &p3, &p4, &p5}) require.NoError(t, err) require.Equal(t, -1, errIdx) @@ -425,7 +425,7 @@ func testPostPersistentNotificationStoreDelete(t *testing.T, rctx request.CTX, s }, } - _, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2, &p3, &p4, &p5}) + _, errIdx, err := ss.Post().SaveMultiple(rctx, []*model.Post{&p1, &p2, &p3, &p4, &p5}) require.NoError(t, err) require.Equal(t, -1, errIdx) diff --git a/server/channels/store/storetest/post_priority_store.go b/server/channels/store/storetest/post_priority_store.go index 49aa8d1ccc..c95128bef1 100644 --- a/server/channels/store/storetest/post_priority_store.go +++ b/server/channels/store/storetest/post_priority_store.go @@ -51,7 +51,7 @@ func testPostPriorityStoreGetForPost(t *testing.T, rctx request.CTX, ss store.St p3.UserId = model.NewId() p3.Message = NewTestID() - _, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2, &p3}) + _, errIdx, err := ss.Post().SaveMultiple(rctx, []*model.Post{&p1, &p2, &p3}) require.NoError(t, err) require.Equal(t, -1, errIdx) diff --git a/server/channels/store/storetest/post_store.go b/server/channels/store/storetest/post_store.go index b69ea2969b..0d883c004d 100644 --- a/server/channels/store/storetest/post_store.go +++ b/server/channels/store/storetest/post_store.go @@ -290,7 +290,7 @@ func testPostStoreSaveMultiple(t *testing.T, rctx request.CTX, ss store.Store) { p4.Message = NewTestID() t.Run("Save correctly a new set of posts", func(t *testing.T) { - newPosts, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2, &p3}) + newPosts, errIdx, err := ss.Post().SaveMultiple(rctx, []*model.Post{&p1, &p2, &p3}) require.NoError(t, err) require.Equal(t, -1, errIdx) for _, post := range newPosts { @@ -358,7 +358,7 @@ func testPostStoreSaveMultiple(t *testing.T, rctx request.CTX, ss store.Store) { o4.UserId = model.NewId() o4.Message = NewTestID() - newPosts, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&o1, &o2, &o3, &o4}) + newPosts, errIdx, err := ss.Post().SaveMultiple(rctx, []*model.Post{&o1, &o2, &o3, &o4}) require.NoError(t, err, "couldn't save item") require.Equal(t, -1, errIdx) assert.Len(t, newPosts, 4) @@ -369,7 +369,7 @@ func testPostStoreSaveMultiple(t *testing.T, rctx request.CTX, ss store.Store) { }) t.Run("Try to save mixed, already saved and not saved posts", func(t *testing.T) { - newPosts, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p4, &p3}) + newPosts, errIdx, err := ss.Post().SaveMultiple(rctx, []*model.Post{&p4, &p3}) require.Error(t, err) require.Equal(t, 1, errIdx) require.Nil(t, newPosts) @@ -405,7 +405,7 @@ func testPostStoreSaveMultiple(t *testing.T, rctx request.CTX, ss store.Store) { replyPost.Message = NewTestID() replyPost.RootId = rootPost.Id - _, _, err = ss.Post().SaveMultiple([]*model.Post{&rootPost, &replyPost}) + _, _, err = ss.Post().SaveMultiple(rctx, []*model.Post{&rootPost, &replyPost}) require.NoError(t, err) rrootPost, err := ss.Post().GetSingle(rctx, rootPost.Id, false) @@ -427,7 +427,7 @@ func testPostStoreSaveMultiple(t *testing.T, rctx request.CTX, ss store.Store) { // Ensure update does not occur in the same timestamp as creation time.Sleep(time.Millisecond) - _, _, err = ss.Post().SaveMultiple([]*model.Post{&replyPost2, &replyPost3}) + _, _, err = ss.Post().SaveMultiple(rctx, []*model.Post{&replyPost2, &replyPost3}) require.NoError(t, err) rrootPost2, err := ss.Post().GetSingle(rctx, rootPost.Id, false) @@ -460,7 +460,7 @@ func testPostStoreSaveMultiple(t *testing.T, rctx request.CTX, ss store.Store) { post3.UserId = model.NewId() post3.Message = NewTestID() - _, _, err = ss.Post().SaveMultiple([]*model.Post{&post1, &post2, &post3}) + _, _, err = ss.Post().SaveMultiple(rctx, []*model.Post{&post1, &post2, &post3}) require.NoError(t, err) rchannel, err := ss.Channel().Get(channel.Id, false) @@ -3772,7 +3772,7 @@ func testPostStoreOverwriteMultiple(t *testing.T, rctx request.CTX, ss store.Sto o3a := ro3.Clone() o3a.Message = ro3.Message + "WWWWWWW" - _, errIdx, err := ss.Post().OverwriteMultiple([]*model.Post{o1a, o2a, o3a}) + _, errIdx, err := ss.Post().OverwriteMultiple(rctx, []*model.Post{o1a, o2a, o3a}) require.NoError(t, err) require.Equal(t, -1, errIdx) @@ -3802,7 +3802,7 @@ func testPostStoreOverwriteMultiple(t *testing.T, rctx request.CTX, ss store.Sto o5a.Filenames = []string{} o5a.FileIds = []string{} - _, errIdx, err := ss.Post().OverwriteMultiple([]*model.Post{o4a, o5a}) + _, errIdx, err := ss.Post().OverwriteMultiple(rctx, []*model.Post{o4a, o5a}) require.NoError(t, err) require.Equal(t, -1, errIdx) diff --git a/server/channels/store/storetest/thread_store.go b/server/channels/store/storetest/thread_store.go index acc920532b..eef636cc6d 100644 --- a/server/channels/store/storetest/thread_store.go +++ b/server/channels/store/storetest/thread_store.go @@ -93,7 +93,7 @@ func testThreadStorePopulation(t *testing.T, rctx request.CTX, ss store.Store) { o4.UserId = model.NewId() o4.Message = NewTestID() - newPosts, errIdx, err3 := ss.Post().SaveMultiple([]*model.Post{&o2, &o3, &o4}) + newPosts, errIdx, err3 := ss.Post().SaveMultiple(rctx, []*model.Post{&o2, &o3, &o4}) opts := model.GetPostsOptions{ SkipFetchThreads: true, @@ -135,7 +135,7 @@ func testThreadStorePopulation(t *testing.T, rctx request.CTX, ss store.Store) { o5.RootId = newPosts[0].Id o5.Message = NewTestID() - _, _, err = ss.Post().SaveMultiple([]*model.Post{&o5}) + _, _, err = ss.Post().SaveMultiple(rctx, []*model.Post{&o5}) require.NoError(t, err, "couldn't save item") thread, err = ss.Thread().Get(newPosts[0].Id) @@ -185,7 +185,7 @@ func testThreadStorePopulation(t *testing.T, rctx request.CTX, ss store.Store) { replyPost.Message = NewTestID() replyPost.RootId = rootPost.RootId - newPosts, _, err := ss.Post().SaveMultiple([]*model.Post{&rootPost, &replyPost}) + newPosts, _, err := ss.Post().SaveMultiple(rctx, []*model.Post{&rootPost, &replyPost}) require.NoError(t, err) thread1, err := ss.Thread().Get(newPosts[0].RootId) @@ -207,7 +207,7 @@ func testThreadStorePopulation(t *testing.T, rctx request.CTX, ss store.Store) { replyPost3.Message = NewTestID() replyPost3.RootId = rootPost.Id - _, _, err = ss.Post().SaveMultiple([]*model.Post{&replyPost2, &replyPost3}) + _, _, err = ss.Post().SaveMultiple(rctx, []*model.Post{&replyPost2, &replyPost3}) require.NoError(t, err) rrootPost2, err := ss.Post().GetSingle(rctx, rootPost.Id, false) @@ -295,7 +295,7 @@ func testThreadStorePopulation(t *testing.T, rctx request.CTX, ss store.Store) { rootPost.UserId = model.NewId() rootPost.Message = NewTestID() - newPosts1, _, err := ss.Post().SaveMultiple([]*model.Post{&rootPost}) + newPosts1, _, err := ss.Post().SaveMultiple(rctx, []*model.Post{&rootPost}) require.NoError(t, err) replyPost := model.Post{} @@ -304,7 +304,7 @@ func testThreadStorePopulation(t *testing.T, rctx request.CTX, ss store.Store) { replyPost.Message = NewTestID() replyPost.RootId = newPosts1[0].Id - _, _, err = ss.Post().SaveMultiple([]*model.Post{&replyPost}) + _, _, err = ss.Post().SaveMultiple(rctx, []*model.Post{&replyPost}) require.NoError(t, err) thread1, err := ss.Thread().Get(newPosts1[0].Id) diff --git a/server/channels/store/timerlayer/timerlayer.go b/server/channels/store/timerlayer/timerlayer.go index 89524e0344..af46ab3b49 100644 --- a/server/channels/store/timerlayer/timerlayer.go +++ b/server/channels/store/timerlayer/timerlayer.go @@ -6433,10 +6433,10 @@ func (s *TimerLayerPostStore) Overwrite(rctx request.CTX, post *model.Post) (*mo return result, err } -func (s *TimerLayerPostStore) OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, error) { +func (s *TimerLayerPostStore) OverwriteMultiple(rctx request.CTX, posts []*model.Post) ([]*model.Post, int, error) { start := time.Now() - result, resultVar1, err := s.PostStore.OverwriteMultiple(posts) + result, resultVar1, err := s.PostStore.OverwriteMultiple(rctx, posts) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -6561,10 +6561,10 @@ func (s *TimerLayerPostStore) Save(rctx request.CTX, post *model.Post) (*model.P return result, err } -func (s *TimerLayerPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, error) { +func (s *TimerLayerPostStore) SaveMultiple(rctx request.CTX, posts []*model.Post) ([]*model.Post, int, error) { start := time.Now() - result, resultVar1, err := s.PostStore.SaveMultiple(posts) + result, resultVar1, err := s.PostStore.SaveMultiple(rctx, posts) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { diff --git a/server/enterprise/elasticsearch/common/common.go b/server/enterprise/elasticsearch/common/common.go index 0e0f8fd189..7d3bec19b6 100644 --- a/server/enterprise/elasticsearch/common/common.go +++ b/server/enterprise/elasticsearch/common/common.go @@ -110,7 +110,7 @@ func ESPostFromPostForIndexing(post *model.PostForIndexing) *ESPost { var searchAttachments []string - if attachments := post.GetProp("attachments"); attachments != nil { + if attachments := post.GetProp(model.PostPropsAttachments); attachments != nil { attachmentsInterfaceArray, ok := attachments.([]any) if ok { for _, attachment := range attachmentsInterfaceArray { diff --git a/server/enterprise/message_export/global_relay_export/global_relay_export.go b/server/enterprise/message_export/global_relay_export/global_relay_export.go index b52dd17cf7..070696245b 100644 --- a/server/enterprise/message_export/global_relay_export/global_relay_export.go +++ b/server/enterprise/message_export/global_relay_export/global_relay_export.go @@ -351,12 +351,12 @@ func addPostToChannelExport(rctx request.CTX, channelExport *ChannelExport, post if err != nil { rctx.Logger().Warn("Failed to unmarshal post Props into JSON. Ignoring username override.", mlog.Err(err)) } else { - if overrideUsername, ok := postPropsLocal["override_username"]; ok { + if overrideUsername, ok := postPropsLocal[model.PostPropsOverrideUsername]; ok { postUserName = overrideUsername.(string) } if postUserName == originalUsername { - if overrideUsername, ok := postPropsLocal["webhook_display_name"]; ok { + if overrideUsername, ok := postPropsLocal[model.PostPropsWebhookDisplayName]; ok { postUserName = overrideUsername.(string) } } diff --git a/server/platform/services/slackimport/slackimport.go b/server/platform/services/slackimport/slackimport.go index a91771b3de..469ab6c0be 100644 --- a/server/platform/services/slackimport/slackimport.go +++ b/server/platform/services/slackimport/slackimport.go @@ -392,9 +392,9 @@ func (si *SlackImporter) slackAddPosts(rctx request.CTX, teamId string, channel } props := make(model.StringInterface) - props["override_username"] = sPost.BotUsername + props[model.PostPropsOverrideUsername] = sPost.BotUsername if len(sPost.Attachments) > 0 { - props["attachments"] = sPost.Attachments + props[model.PostPropsAttachments] = sPost.Attachments } post := &model.Post{ @@ -817,19 +817,19 @@ func (si *SlackImporter) oldImportIncomingWebhookPost(rctx request.CTX, post *mo linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) post.Message = linkWithTextRegex.ReplaceAllString(post.Message, "[${2}](${1})") - post.AddProp("from_webhook", "true") + post.AddProp(model.PostPropsFromWebhook, "true") - if _, ok := props["override_username"]; !ok { - post.AddProp("override_username", model.DefaultWebhookUsername) + if _, ok := props[model.PostPropsOverrideUsername]; !ok { + post.AddProp(model.PostPropsOverrideUsername, model.DefaultWebhookUsername) } if len(props) > 0 { for key, val := range props { - if key == "attachments" { + if key == model.PostPropsAttachments { if attachments, success := val.([]*model.SlackAttachment); success { model.ParseSlackAttachment(post, attachments) } - } else if key != "from_webhook" { + } else if key != model.PostPropsFromWebhook { post.AddProp(key, val) } } diff --git a/server/public/model/client4_test.go b/server/public/model/client4_test.go index 4756682d13..27be553f2e 100644 --- a/server/public/model/client4_test.go +++ b/server/public/model/client4_test.go @@ -36,10 +36,11 @@ func TestClient4TrimTrailingSlash(t *testing.T) { func TestClient4CreatePost(t *testing.T) { post := &model.Post{ Props: map[string]any{ - "attachments": []*model.SlackAttachment{ + model.PostPropsAttachments: []*model.SlackAttachment{ { Actions: []*model.PostAction{ { + Type: model.PostActionTypeButton, Integration: &model.PostActionIntegration{ Context: map[string]any{ "foo": "bar", @@ -63,6 +64,7 @@ func TestClient4CreatePost(t *testing.T) { { Actions: []*model.PostAction{ { + Type: model.PostActionTypeButton, Integration: &model.PostActionIntegration{ Context: map[string]any{ "foo": "bar", diff --git a/server/public/model/integration_action.go b/server/public/model/integration_action.go index d46fec1335..a1ac92117f 100644 --- a/server/public/model/integration_action.go +++ b/server/public/model/integration_action.go @@ -17,6 +17,7 @@ import ( "math/big" "net/http" "reflect" + "slices" "strconv" "strings" "time" @@ -38,13 +39,18 @@ const ( DialogElementBoolMaxLength = 150 ) -var PostActionRetainPropKeys = []string{"from_webhook", "override_username", "override_icon_url"} +var PostActionRetainPropKeys = []string{PostPropsFromWebhook, PostPropsOverrideUsername, PostPropsOverrideIconURL} type DoPostActionRequest struct { SelectedOption string `json:"selected_option,omitempty"` Cookie string `json:"cookie,omitempty"` } +const ( + PostActionDataSourceUsers = "users" + PostActionDataSourceChannels = "channels" +) + type PostAction struct { // A unique Action ID. If not set, generated automatically. Id string `json:"id,omitempty"` @@ -85,6 +91,73 @@ type PostAction struct { Cookie string `json:"cookie,omitempty" db:"-"` } +// IsValid validates the action and returns an error if it is invalid. +func (p *PostAction) IsValid() error { + var multiErr *multierror.Error + + if p.Name == "" { + multiErr = multierror.Append(multiErr, fmt.Errorf("action must have a name")) + } + + if p.Style != "" { + validStyles := []string{"default", "primary", "success", "good", "warning", "danger"} + // If not a predefined style, check if it's a hex color + if !slices.Contains(validStyles, p.Style) && !hexColorRegex.MatchString(p.Style) { + multiErr = multierror.Append(multiErr, fmt.Errorf("invalid style '%s' - must be one of [default, primary, success, good, warning, danger] or a hex color", p.Style)) + } + } + + switch p.Type { + case PostActionTypeButton: + if len(p.Options) > 0 { + multiErr = multierror.Append(multiErr, fmt.Errorf("button action must not have options")) + } + if p.DataSource != "" { + multiErr = multierror.Append(multiErr, fmt.Errorf("button action must not have a data source")) + } + case PostActionTypeSelect: + if p.DataSource != "" { + validSources := []string{PostActionDataSourceUsers, PostActionDataSourceChannels} + if !slices.Contains(validSources, p.DataSource) { + multiErr = multierror.Append(multiErr, fmt.Errorf("invalid data_source '%s' for select action", p.DataSource)) + } + + if len(p.Options) > 0 { + multiErr = multierror.Append(multiErr, fmt.Errorf("select action cannot have both DataSource and Options set")) + } + } else { + if len(p.Options) == 0 { + multiErr = multierror.Append(multiErr, fmt.Errorf("select action must have either DataSource or Options set")) + } else { + for i, opt := range p.Options { + if opt == nil { + multiErr = multierror.Append(multiErr, fmt.Errorf("select action contains nil option")) + continue + } + if err := opt.IsValid(); err != nil { + multiErr = multierror.Append(multiErr, multierror.Prefix(err, fmt.Sprintf("option at index %d is invalid:", i))) + } + } + } + } + default: + multiErr = multierror.Append(multiErr, fmt.Errorf("invalid action type: must be '%s' or '%s'", PostActionTypeButton, PostActionTypeSelect)) + } + + if p.Integration == nil { + multiErr = multierror.Append(multiErr, fmt.Errorf("action must have integration settings")) + } else { + if p.Integration.URL == "" { + multiErr = multierror.Append(multiErr, fmt.Errorf("action must have an integration URL")) + } + if !(strings.HasPrefix(p.Integration.URL, "/plugins/") || strings.HasPrefix(p.Integration.URL, "plugins/") || IsValidHTTPURL(p.Integration.URL)) { + multiErr = multierror.Append(multiErr, fmt.Errorf("action must have an valid integration URL")) + } + } + + return multiErr.ErrorOrNil() +} + func (p *PostAction) Equals(input *PostAction) bool { if p.Id != input.Id { return false @@ -189,7 +262,22 @@ type PostActionOptions struct { Value string `json:"value"` } +func (o *PostActionOptions) IsValid() error { + var multiErr *multierror.Error + + if o.Text == "" { + multiErr = multierror.Append(multiErr, fmt.Errorf("text is required")) + } + if o.Value == "" { + multiErr = multierror.Append(multiErr, fmt.Errorf("value is required")) + } + + return multiErr.ErrorOrNil() +} + type PostActionIntegration struct { + // URL is the endpoint that the action will be sent to. + // It can be a relative path to a plugin. URL string `json:"url,omitempty"` Context map[string]any `json:"context,omitempty"` } @@ -474,24 +562,25 @@ func isDefaultInOptions(defaultValue string, options []*PostActionOptions) bool return false } -func checkMaxLength(fieldName string, field string, length int) error { - var valid bool +func checkMaxLength(fieldName string, field string, maxLength int) error { // DisplayName and Name are required fields if fieldName == "DisplayName" || fieldName == "Name" { - valid = len(field) > 0 && len(field) > length - } else { - valid = len(field) > length + if len(field) == 0 { + return errors.Errorf("%v cannot be empty", fieldName) + } } - if valid { - return errors.Errorf("%v cannot be longer than %d characters", fieldName, length) + + if len(field) > maxLength { + return errors.Errorf("%v cannot be longer than %d characters, got %d", fieldName, maxLength, len(field)) } + return nil } func (o *Post) StripActionIntegrations() { attachments := o.Attachments() - if o.GetProp("attachments") != nil { - o.AddProp("attachments", attachments) + if o.GetProp(PostPropsAttachments) != nil { + o.AddProp(PostPropsAttachments, attachments) } for _, attachment := range attachments { for _, action := range attachment.Actions { @@ -512,10 +601,10 @@ func (o *Post) GetAction(id string) *PostAction { } func (o *Post) GenerateActionIds() { - if o.GetProp("attachments") != nil { - o.AddProp("attachments", o.Attachments()) + if o.GetProp(PostPropsAttachments) != nil { + o.AddProp(PostPropsAttachments, o.Attachments()) } - if attachments, ok := o.GetProp("attachments").([]*SlackAttachment); ok { + if attachments, ok := o.GetProp(PostPropsAttachments).([]*SlackAttachment); ok { for _, attachment := range attachments { for _, action := range attachment.Actions { if action != nil && action.Id == "" { diff --git a/server/public/model/integration_action_test.go b/server/public/model/integration_action_test.go index cbdbe041be..039459ba30 100644 --- a/server/public/model/integration_action_test.go +++ b/server/public/model/integration_action_test.go @@ -16,6 +16,246 @@ import ( "github.com/stretchr/testify/require" ) +func TestPostAction_IsValid(t *testing.T) { + tests := map[string]struct { + action *PostAction + wantErr string + }{ + "valid button action with http URL": { + action: &PostAction{ + Id: "validid", + Name: "Test Button", + Type: PostActionTypeButton, + Integration: &PostActionIntegration{ + URL: "http://localhost:8065", + }, + }, + wantErr: "", + }, + "valid button action with http URL without Id": { + action: &PostAction{ + Name: "Test Button", + Type: PostActionTypeButton, + Integration: &PostActionIntegration{ + URL: "http://localhost:8065", + }, + }, + wantErr: "", + }, + "valid button action with plugin path": { + action: &PostAction{ + Id: "validid", + Name: "Test Button", + Type: PostActionTypeButton, + Integration: &PostActionIntegration{ + URL: "/plugins/myplugin/action", + }, + }, + wantErr: "", + }, + "valid button action with relative plugin path": { + action: &PostAction{ + Id: "validid", + Name: "Test Button", + Type: PostActionTypeButton, + Integration: &PostActionIntegration{ + URL: "plugins/myplugin/action", + }, + }, + wantErr: "", + }, + "invalid integration URL": { + action: &PostAction{ + Id: "validid", + Name: "Test Button", + Type: PostActionTypeButton, + Integration: &PostActionIntegration{ + URL: "invalid-url", + }, + }, + wantErr: "action must have an valid integration URL", + }, + "valid select action with datasource": { + action: &PostAction{ + Id: "validid", + Name: "Test Select", + Type: PostActionTypeSelect, + DataSource: PostActionDataSourceUsers, + Integration: &PostActionIntegration{ + URL: "http://localhost:8065", + }, + }, + wantErr: "", + }, + "valid select action with options": { + action: &PostAction{ + Id: "validid", + Name: "Test Select", + Type: PostActionTypeSelect, + Options: []*PostActionOptions{ + {Text: "Opt1", Value: "opt1"}, + }, + Integration: &PostActionIntegration{ + URL: "http://localhost:8065", + }, + }, + wantErr: "", + }, + "select action with nil option": { + action: &PostAction{ + Id: "validid", + Name: "Test Select", + Type: PostActionTypeSelect, + Options: []*PostActionOptions{ + nil, + {Text: "Opt1", Value: "opt1"}, + }, + Integration: &PostActionIntegration{ + URL: "http://localhost:8065", + }, + }, + wantErr: "select action contains nil option", + }, + "missing name": { + action: &PostAction{ + Id: "validid", + Type: PostActionTypeButton, + Integration: &PostActionIntegration{ + URL: "http://localhost:8065", + }, + }, + wantErr: "action must have a name", + }, + "invalid style": { + action: &PostAction{ + Id: "validid", + Name: "Test Button", + Type: PostActionTypeButton, + Style: "invalid", + Integration: &PostActionIntegration{ + URL: "http://localhost:8065", + }, + }, + wantErr: "invalid style 'invalid' - must be one of [default, primary, success, good, warning, danger] or a hex color", + }, + "valid style": { + action: &PostAction{ + Id: "validid", + Name: "Test Button", + Type: PostActionTypeButton, + Style: "primary", + Integration: &PostActionIntegration{ + URL: "http://localhost:8065", + }, + }, + wantErr: "", + }, + "button with options": { + action: &PostAction{ + Id: "validid", + Name: "Test Button", + Type: PostActionTypeButton, + Options: []*PostActionOptions{ + {Text: "Opt1", Value: "opt1"}, + }, + Integration: &PostActionIntegration{ + URL: "http://localhost:8065", + }, + }, + wantErr: "button action must not have options", + }, + "button with datasource": { + action: &PostAction{ + Id: "validid", + Name: "Test Button", + Type: PostActionTypeButton, + DataSource: PostActionDataSourceUsers, + Integration: &PostActionIntegration{ + URL: "http://localhost:8065", + }, + }, + wantErr: "button action must not have a data source", + }, + "select without datasource or options": { + action: &PostAction{ + Id: "validid", + Name: "Test Select", + Type: PostActionTypeSelect, + Integration: &PostActionIntegration{ + URL: "http://localhost:8065", + }, + }, + wantErr: "select action must have either DataSource or Options set", + }, + "select with both datasource and options": { + action: &PostAction{ + Id: "validid", + Name: "Test Select", + Type: PostActionTypeSelect, + DataSource: PostActionDataSourceUsers, + Options: []*PostActionOptions{ + {Text: "Opt1", Value: "opt1"}, + }, + Integration: &PostActionIntegration{ + URL: "http://localhost:8065", + }, + }, + wantErr: "select action cannot have both DataSource and Options set", + }, + "invalid datasource": { + action: &PostAction{ + Id: "validid", + Name: "Test Select", + Type: PostActionTypeSelect, + DataSource: "invalid", + Integration: &PostActionIntegration{ + URL: "http://localhost:8065", + }, + }, + wantErr: "invalid data_source 'invalid' for select action", + }, + "missing integration": { + action: &PostAction{ + Id: "validid", + Name: "Test Button", + Type: PostActionTypeButton, + }, + wantErr: "action must have integration settings", + }, + "missing integration URL": { + action: &PostAction{ + Id: "validid", + Name: "Test Button", + Type: PostActionTypeButton, + Integration: &PostActionIntegration{}, + }, + wantErr: "action must have an integration URL", + }, + "invalid type": { + action: &PostAction{ + Id: "validid", + Name: "Test Action", + Type: "invalid", + Integration: &PostActionIntegration{ + URL: "http://localhost:8065", + }, + }, + wantErr: "invalid action type: must be 'button' or 'select'", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + err := tc.action.IsValid() + if tc.wantErr == "" { + assert.NoError(t, err, name) + } else { + assert.ErrorContains(t, err, tc.wantErr, name) + } + }) + } +} + func TestTriggerIdDecodeAndVerification(t *testing.T) { key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) @@ -182,6 +422,44 @@ func TestPostActionIntegrationEquals(t *testing.T) { }) } +func TestPostActionOptions_IsValid(t *testing.T) { + tests := map[string]struct { + options *PostActionOptions + wantErr string + }{ + "valid options": { + options: &PostActionOptions{ + Text: "Option 1", + Value: "opt1", + }, + wantErr: "", + }, + "missing text": { + options: &PostActionOptions{ + Value: "opt1", + }, + wantErr: "text is required", + }, + "missing value": { + options: &PostActionOptions{ + Text: "Option 1", + }, + wantErr: "value is required", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + err := tc.options.IsValid() + if tc.wantErr == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, tc.wantErr) + } + }) + } +} + func TestOpenDialogRequestIsValid(t *testing.T) { getBaseOpenDialogRequest := func() OpenDialogRequest { return OpenDialogRequest{ diff --git a/server/public/model/post.go b/server/public/model/post.go index b7a544e140..fc12f386e2 100644 --- a/server/public/model/post.go +++ b/server/public/model/post.go @@ -15,7 +15,9 @@ import ( "sync" "unicode/utf8" + "github.com/hashicorp/go-multierror" "github.com/mattermost/mattermost/server/public/shared/markdown" + "github.com/mattermost/mattermost/server/public/shared/mlog" ) const ( @@ -72,14 +74,16 @@ const ( PostPropsFromBot = "from_bot" PostPropsFromOAuthApp = "from_oauth_app" PostPropsWebhookDisplayName = "webhook_display_name" + PostPropsAttachments = "attachments" + PostPropsFromPlugin = "from_plugin" PostPropsMentionHighlightDisabled = "mentionHighlightDisabled" PostPropsGroupHighlightDisabled = "disable_group_highlight" PostPropsPreviewedPost = "previewed_post" PostPropsForceNotification = "force_notification" + PostPropsChannelMentions = "channel_mentions" + PostPropsUnsafeLinks = "unsafe_links" - PostPriorityUrgent = "urgent" - PostPropsRequestedAck = "requested_ack" - PostPropsPersistentNotifications = "persistent_notifications" + PostPriorityUrgent = "urgent" ) type Post struct { @@ -657,6 +661,137 @@ func (o *Post) GetProp(key string) any { return o.Props[key] } +// ValidateProps checks all known props for validity. +// Currently, it logs warnings for invalid props rather than returning an error. +// In a future version, this will be updated to return errors for invalid props. +func (o *Post) ValidateProps(logger mlog.LoggerIFace) { + if err := o.propsIsValid(); err != nil { + logger.Warn( + "Invalid post props. In a future version this will result in an error. Please update your integration to be compliant.", + mlog.String("post_id", o.Id), + mlog.Err(err), + ) + } +} + +func (o *Post) propsIsValid() error { + var multiErr *multierror.Error + + props := o.GetProps() + + // Check basic props validity + if props == nil { + return nil + } + + if props[PostPropsAddedUserId] != nil { + if addedUserID, ok := props[PostPropsAddedUserId].(string); !ok { + multiErr = multierror.Append(multiErr, fmt.Errorf("added_user_id prop must be a string")) + } else if !IsValidId(addedUserID) { + multiErr = multierror.Append(multiErr, fmt.Errorf("added_user_id prop must be a valid user ID")) + } + } + if props[PostPropsDeleteBy] != nil { + if deleteByID, ok := props[PostPropsDeleteBy].(string); !ok { + multiErr = multierror.Append(multiErr, fmt.Errorf("delete_by prop must be a string")) + } else if !IsValidId(deleteByID) { + multiErr = multierror.Append(multiErr, fmt.Errorf("delete_by prop must be a valid user ID")) + } + } + + // Validate integration props + if props[PostPropsOverrideIconURL] != nil { + if iconURL, ok := props[PostPropsOverrideIconURL].(string); !ok { + multiErr = multierror.Append(multiErr, fmt.Errorf("override_icon_url prop must be a string")) + } else if iconURL == "" || !IsValidHTTPURL(iconURL) { + multiErr = multierror.Append(multiErr, fmt.Errorf("override_icon_url prop must be a valid URL")) + } + } + if props[PostPropsOverrideIconEmoji] != nil { + if _, ok := props[PostPropsOverrideIconEmoji].(string); !ok { + multiErr = multierror.Append(multiErr, fmt.Errorf("override_icon_emoji prop must be a string")) + } + } + if props[PostPropsOverrideUsername] != nil { + if _, ok := props[PostPropsOverrideUsername].(string); !ok { + multiErr = multierror.Append(multiErr, fmt.Errorf("override_username prop must be a string")) + } + } + if props[PostPropsFromWebhook] != nil { + if fromWebhook, ok := props[PostPropsFromWebhook].(string); !ok { + multiErr = multierror.Append(multiErr, fmt.Errorf("from_webhook prop must be a string")) + } else if fromWebhook != "true" { + multiErr = multierror.Append(multiErr, fmt.Errorf("from_webhook prop must be \"true\"")) + } + } + if props[PostPropsFromBot] != nil { + if fromBot, ok := props[PostPropsFromBot].(string); !ok { + multiErr = multierror.Append(multiErr, fmt.Errorf("from_bot prop must be a string")) + } else if fromBot != "true" { + multiErr = multierror.Append(multiErr, fmt.Errorf("from_bot prop must be \"true\"")) + } + } + if props[PostPropsFromOAuthApp] != nil { + if fromOAuthApp, ok := props[PostPropsFromOAuthApp].(string); !ok { + multiErr = multierror.Append(multiErr, fmt.Errorf("from_oauth_app prop must be a string")) + } else if fromOAuthApp != "true" { + multiErr = multierror.Append(multiErr, fmt.Errorf("from_oauth_app prop must be \"true\"")) + } + } + if props[PostPropsFromPlugin] != nil { + if fromPlugin, ok := props[PostPropsFromPlugin].(string); !ok { + multiErr = multierror.Append(multiErr, fmt.Errorf("from_plugin prop must be a string")) + } else if fromPlugin != "true" { + multiErr = multierror.Append(multiErr, fmt.Errorf("from_plugin prop must be \"true\"")) + } + } + if props[PostPropsUnsafeLinks] != nil { + if unsafeLinks, ok := props[PostPropsUnsafeLinks].(string); !ok { + multiErr = multierror.Append(multiErr, fmt.Errorf("unsafe_links prop must be a string")) + } else if unsafeLinks != "true" { + multiErr = multierror.Append(multiErr, fmt.Errorf("unsafe_links prop must be \"true\"")) + } + } + if props[PostPropsWebhookDisplayName] != nil { + if _, ok := props[PostPropsWebhookDisplayName].(string); !ok { + multiErr = multierror.Append(multiErr, fmt.Errorf("webhook_display_name prop must be a string")) + } + } + + if props[PostPropsMentionHighlightDisabled] != nil { + if _, ok := props[PostPropsMentionHighlightDisabled].(bool); !ok { + multiErr = multierror.Append(multiErr, fmt.Errorf("mention_highlight_disabled prop must be a boolean")) + } + } + if props[PostPropsGroupHighlightDisabled] != nil { + if _, ok := props[PostPropsGroupHighlightDisabled].(bool); !ok { + multiErr = multierror.Append(multiErr, fmt.Errorf("disable_group_highlight prop must be a boolean")) + } + } + + if props[PostPropsPreviewedPost] != nil { + if previewedPostID, ok := props[PostPropsPreviewedPost].(string); !ok { + multiErr = multierror.Append(multiErr, fmt.Errorf("previewed_post prop must be a string")) + } else if !IsValidId(previewedPostID) { + multiErr = multierror.Append(multiErr, fmt.Errorf("previewed_post prop must be a valid post ID")) + } + } + + if props[PostPropsForceNotification] != nil { + if _, ok := props[PostPropsForceNotification].(bool); !ok { + multiErr = multierror.Append(multiErr, fmt.Errorf("force_notification prop must be a boolean")) + } + } + + for i, a := range o.Attachments() { + if err := a.IsValid(); err != nil { + multiErr = multierror.Append(multiErr, multierror.Prefix(err, fmt.Sprintf("message attachtment at index %d is invalid:", i))) + } + } + + return multiErr.ErrorOrNil() +} + func (o *Post) IsSystemMessage() bool { return len(o.Type) >= len(PostSystemMessagePrefix) && o.Type[:len(PostSystemMessagePrefix)] == PostSystemMessagePrefix } @@ -746,11 +881,11 @@ func findAtChannelMention(message string) (mention string, found bool) { } func (o *Post) Attachments() []*SlackAttachment { - if attachments, ok := o.GetProp("attachments").([]*SlackAttachment); ok { + if attachments, ok := o.GetProp(PostPropsAttachments).([]*SlackAttachment); ok { return attachments } var ret []*SlackAttachment - if attachments, ok := o.GetProp("attachments").([]any); ok { + if attachments, ok := o.GetProp(PostPropsAttachments).([]any); ok { for _, attachment := range attachments { if enc, err := json.Marshal(attachment); err == nil { var decoded SlackAttachment @@ -885,7 +1020,7 @@ func RewriteImageURLs(message string, f func(string) string) string { func (o *Post) IsFromOAuthBot() bool { props := o.GetProps() - return props["from_webhook"] == "true" && props["override_username"] != "" + return props[PostPropsFromWebhook] == "true" && props[PostPropsOverrideUsername] != "" } func (o *Post) ToNilIfInvalid() *Post { diff --git a/server/public/model/post_test.go b/server/public/model/post_test.go index 4d2cc679cc..eb99d0a3bc 100644 --- a/server/public/model/post_test.go +++ b/server/public/model/post_test.go @@ -140,7 +140,7 @@ func TestPostSanitizeProps(t *testing.T) { Props: StringInterface{ PropsAddChannelMember: "no good", PostPropsForceNotification: "no good", - "attachments": "good", + PostPropsAttachments: "good", }, } @@ -149,7 +149,7 @@ func TestPostSanitizeProps(t *testing.T) { require.Nil(t, post3.GetProp(PropsAddChannelMember)) require.Nil(t, post3.GetProp(PostPropsForceNotification)) - require.NotNil(t, post3.GetProp("attachments")) + require.NotNil(t, post3.GetProp(PostPropsAttachments)) } func TestPost_ContainsIntegrationsReservedProps(t *testing.T) { @@ -162,11 +162,11 @@ func TestPost_ContainsIntegrationsReservedProps(t *testing.T) { post2 := &Post{ Message: "test", Props: StringInterface{ - "from_webhook": "true", - "webhook_display_name": "overridden_display_name", - "override_username": "overridden_username", - "override_icon_url": "a-custom-url", - "override_icon_emoji": ":custom_emoji_name:", + PostPropsFromWebhook: "true", + PostPropsWebhookDisplayName: "overridden_display_name", + PostPropsOverrideUsername: "overridden_username", + PostPropsOverrideIconURL: "a-custom-url", + PostPropsOverrideIconEmoji: ":custom_emoji_name:", }, } keys2 := post2.ContainsIntegrationsReservedProps() @@ -176,7 +176,7 @@ func TestPost_ContainsIntegrationsReservedProps(t *testing.T) { func TestPostPatch_ContainsIntegrationsReservedProps(t *testing.T) { postPatch1 := &PostPatch{ Props: &StringInterface{ - "from_webhook": "true", + PostPropsFromWebhook: "true", }, } keys1 := postPatch1.ContainsIntegrationsReservedProps() @@ -405,8 +405,8 @@ func TestPost_AttachmentsEqual(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { - post1.AddProp("attachments", tc.Attachments1) - post2.AddProp("attachments", tc.Attachments2) + post1.AddProp(PostPropsAttachments, tc.Attachments1) + post2.AddProp(PostPropsAttachments, tc.Attachments2) assert.Equal(t, tc.Expected, post1.AttachmentsEqual(post2)) }) } @@ -895,7 +895,7 @@ func TestPostPatchDisableMentionHighlights(t *testing.T) { func TestPostAttachments(t *testing.T) { p := &Post{ Props: map[string]any{ - "attachments": []byte(`[{ + PostPropsAttachments: []byte(`[{ "actions" : {null} }] `), @@ -903,7 +903,7 @@ func TestPostAttachments(t *testing.T) { } t.Run("empty actions", func(t *testing.T) { - p.Props["attachments"] = []any{ + p.Props[PostPropsAttachments] = []any{ map[string]any{"actions": []any{}}, } attachments := p.Attachments() @@ -911,7 +911,7 @@ func TestPostAttachments(t *testing.T) { }) t.Run("a couple of actions", func(t *testing.T) { - p.Props["attachments"] = []any{ + p.Props[PostPropsAttachments] = []any{ map[string]any{"actions": []any{ map[string]any{"id": "test1"}, map[string]any{"id": "test2"}}, }, @@ -924,7 +924,7 @@ func TestPostAttachments(t *testing.T) { }) t.Run("should ignore null actions", func(t *testing.T) { - p.Props["attachments"] = []any{ + p.Props[PostPropsAttachments] = []any{ map[string]any{"actions": []any{ map[string]any{"id": "test1"}, nil, map[string]any{"id": "test2"}, nil, nil}, }, @@ -937,7 +937,7 @@ func TestPostAttachments(t *testing.T) { }) t.Run("nil fields", func(t *testing.T) { - p.Props["attachments"] = []any{ + p.Props[PostPropsAttachments] = []any{ map[string]any{"fields": []any{ map[string]any{"value": ":emoji1:"}, nil, @@ -995,3 +995,278 @@ func TestPostPriority(t *testing.T) { p.Metadata.Priority.Priority = NewPointer(PostPriorityUrgent) require.True(t, p.IsUrgent()) } + +func TestPost_PropsIsValid(t *testing.T) { + tests := map[string]struct { + props StringInterface + wantErr string + }{ + "valid empty props": { + props: nil, + wantErr: "", + }, + "valid props": { + props: StringInterface{ + "key": "value", + }, + wantErr: "", + }, + "valid added_user_id": { + props: StringInterface{ + PostPropsAddedUserId: NewId(), + }, + wantErr: "", + }, + "valid delete_by": { + props: StringInterface{ + PostPropsDeleteBy: NewId(), + }, + wantErr: "", + }, + "valid override_icon_url": { + props: StringInterface{ + PostPropsOverrideIconURL: "https://example.com/icon.png", + }, + wantErr: "", + }, + "valid override_icon_emoji": { + props: StringInterface{ + PostPropsOverrideIconEmoji: ":smile:", + }, + wantErr: "", + }, + "valid override_username": { + props: StringInterface{ + PostPropsOverrideUsername: "testuser", + }, + wantErr: "", + }, + "valid from_webhook": { + props: StringInterface{ + PostPropsFromWebhook: "true", + }, + wantErr: "", + }, + "valid from_bot": { + props: StringInterface{ + PostPropsFromBot: "true", + }, + wantErr: "", + }, + "valid from_oauth_app": { + props: StringInterface{ + PostPropsFromOAuthApp: "true", + }, + wantErr: "", + }, + "valid from_plugin": { + props: StringInterface{ + PostPropsFromPlugin: "true", + }, + wantErr: "", + }, + "valid unsafe_links": { + props: StringInterface{ + PostPropsUnsafeLinks: "true", + }, + wantErr: "", + }, + "valid webhook_display_name": { + props: StringInterface{ + PostPropsWebhookDisplayName: "My Webhook", + }, + wantErr: "", + }, + "valid mention_highlight_disabled": { + props: StringInterface{ + PostPropsMentionHighlightDisabled: true, + }, + wantErr: "", + }, + "valid disable_group_highlight": { + props: StringInterface{ + PostPropsGroupHighlightDisabled: true, + }, + wantErr: "", + }, + "valid previewed_post": { + props: StringInterface{ + PostPropsPreviewedPost: NewId(), + }, + wantErr: "", + }, + "valid force_notification": { + props: StringInterface{ + PostPropsForceNotification: true, + }, + wantErr: "", + }, + "valid multiple props": { + props: StringInterface{ + PostPropsFromWebhook: "true", + PostPropsOverrideUsername: "webhook-user", + PostPropsOverrideIconURL: "https://example.com/icon.png", + PostPropsWebhookDisplayName: "My Webhook", + PostPropsMentionHighlightDisabled: true, + }, + wantErr: "", + }, + "invalid added_user_id type": { + props: StringInterface{ + PostPropsAddedUserId: 123, + }, + wantErr: "added_user_id prop must be a string", + }, + "invalid added_user_id value": { + props: StringInterface{ + PostPropsAddedUserId: "invalid-id", + }, + wantErr: "added_user_id prop must be a valid user ID", + }, + "invalid delete_by type": { + props: StringInterface{ + PostPropsDeleteBy: 123, + }, + wantErr: "delete_by prop must be a string", + }, + "invalid delete_by value": { + props: StringInterface{ + PostPropsDeleteBy: "invalid-id", + }, + wantErr: "delete_by prop must be a valid user ID", + }, + "invalid override_icon_url type": { + props: StringInterface{ + PostPropsOverrideIconURL: 123, + }, + wantErr: "override_icon_url prop must be a string", + }, + "invalid override_icon_url value": { + props: StringInterface{ + PostPropsOverrideIconURL: "not-a-url", + }, + wantErr: "override_icon_url prop must be a valid URL", + }, + "invalid override_icon_emoji type": { + props: StringInterface{ + PostPropsOverrideIconEmoji: 123, + }, + wantErr: "override_icon_emoji prop must be a string", + }, + "invalid override_username type": { + props: StringInterface{ + PostPropsOverrideUsername: 123, + }, + wantErr: "override_username prop must be a string", + }, + "invalid from_webhook type": { + props: StringInterface{ + PostPropsFromWebhook: 123, + }, + wantErr: "from_webhook prop must be a string", + }, + "invalid from_webhook value": { + props: StringInterface{ + PostPropsFromWebhook: "false", + }, + wantErr: "from_webhook prop must be \"true\"", + }, + "invalid from_bot type": { + props: StringInterface{ + PostPropsFromBot: 123, + }, + wantErr: "from_bot prop must be a string", + }, + "invalid from_bot value": { + props: StringInterface{ + PostPropsFromBot: "false", + }, + wantErr: "from_bot prop must be \"true\"", + }, + "invalid from_oauth_app type": { + props: StringInterface{ + PostPropsFromOAuthApp: 123, + }, + wantErr: "from_oauth_app prop must be a string", + }, + "invalid from_oauth_app value": { + props: StringInterface{ + PostPropsFromOAuthApp: "false", + }, + wantErr: "from_oauth_app prop must be \"true\"", + }, + "invalid from_plugin type": { + props: StringInterface{ + PostPropsFromPlugin: 123, + }, + wantErr: "from_plugin prop must be a string", + }, + "invalid from_plugin value": { + props: StringInterface{ + PostPropsFromPlugin: "false", + }, + wantErr: "from_plugin prop must be \"true\"", + }, + "invalid unsafe_links type": { + props: StringInterface{ + PostPropsUnsafeLinks: 123, + }, + wantErr: "unsafe_links prop must be a string", + }, + "invalid unsafe_links value": { + props: StringInterface{ + PostPropsUnsafeLinks: "false", + }, + wantErr: "unsafe_links prop must be \"true\"", + }, + "invalid webhook_display_name type": { + props: StringInterface{ + PostPropsWebhookDisplayName: 123, + }, + wantErr: "webhook_display_name prop must be a string", + }, + "invalid mention_highlight_disabled type": { + props: StringInterface{ + PostPropsMentionHighlightDisabled: "true", + }, + wantErr: "mention_highlight_disabled prop must be a boolean", + }, + "invalid disable_group_highlight type": { + props: StringInterface{ + PostPropsGroupHighlightDisabled: "true", + }, + wantErr: "disable_group_highlight prop must be a boolean", + }, + "invalid previewed_post type": { + props: StringInterface{ + PostPropsPreviewedPost: 123, + }, + wantErr: "previewed_post prop must be a string", + }, + "invalid previewed_post value": { + props: StringInterface{ + PostPropsPreviewedPost: "invalid-id", + }, + wantErr: "previewed_post prop must be a valid post ID", + }, + "invalid force_notification type": { + props: StringInterface{ + PostPropsForceNotification: "true", + }, + wantErr: "force_notification prop must be a boolean", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + p := &Post{} + p.SetProps(tc.props) + err := p.propsIsValid() + if tc.wantErr == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, tc.wantErr) + } + }) + } +} diff --git a/server/public/model/slack_attachment.go b/server/public/model/slack_attachment.go index 60c9667404..96167230b4 100644 --- a/server/public/model/slack_attachment.go +++ b/server/public/model/slack_attachment.go @@ -6,9 +6,15 @@ package model import ( "fmt" "regexp" + "slices" + + "github.com/hashicorp/go-multierror" ) -var linkWithTextRegex = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) +var ( + linkWithTextRegex = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) + hexColorRegex = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`) +) type SlackAttachment struct { Id int64 `json:"id"` @@ -30,6 +36,78 @@ type SlackAttachment struct { Actions []*PostAction `json:"actions,omitempty"` } +func (s *SlackAttachment) IsValid() error { + var multiErr *multierror.Error + + if s.Color != "" { + validStyles := []string{"good", "warning", "danger"} + // If not a predefined style, check if it's a hex color + if !slices.Contains(validStyles, s.Color) && !hexColorRegex.MatchString(s.Color) { + multiErr = multierror.Append(multiErr, fmt.Errorf("invalid style '%s' - must be one of [good, warning, danger] or a hex color", s.Color)) + } + } + + if s.AuthorLink != "" { + if s.AuthorName == "" { + multiErr = multierror.Append(multiErr, fmt.Errorf("author link cannot be set without author name")) + } + + if !IsValidHTTPURL(s.AuthorLink) { + multiErr = multierror.Append(multiErr, fmt.Errorf("invalid author link URL")) + } + } + + if s.AuthorIcon != "" && !IsValidHTTPURL(s.AuthorIcon) { + multiErr = multierror.Append(multiErr, fmt.Errorf("invalid author icon URL")) + } + + if s.TitleLink != "" { + if s.Title == "" { + multiErr = multierror.Append(multiErr, fmt.Errorf("title link cannot be set without title")) + } + + if !IsValidHTTPURL(s.TitleLink) { + multiErr = multierror.Append(multiErr, fmt.Errorf("invalid title link URL")) + } + } + + for _, field := range s.Fields { + if err := field.IsValid(); err != nil { + multiErr = multierror.Append(multiErr, err) + } + } + + if s.ImageURL != "" && !IsValidHTTPURL(s.ImageURL) { + multiErr = multierror.Append(multiErr, fmt.Errorf("invalid image URL")) + } + + if s.ThumbURL != "" && !IsValidHTTPURL(s.ThumbURL) { + multiErr = multierror.Append(multiErr, fmt.Errorf("invalid thumb URL")) + } + + if s.FooterIcon != "" && !IsValidHTTPURL(s.FooterIcon) { + multiErr = multierror.Append(multiErr, fmt.Errorf("invalid footer icon URL")) + } + + // Validate timestamp is either string or int64 + if s.Timestamp != nil { + switch s.Timestamp.(type) { + case string, int64: + // Valid types + default: + multiErr = multierror.Append(multiErr, fmt.Errorf("timestamp must be either a string or int64")) + } + } + + for i, action := range s.Actions { + if err := action.IsValid(); err != nil { + multiErr = multierror.Append(multiErr, multierror.Prefix(err, fmt.Sprintf("action at index %d is invalid:", i))) + } + } + + return multiErr.ErrorOrNil() +} + func (s *SlackAttachment) Equals(input *SlackAttachment) bool { // Direct comparison of simple types @@ -120,6 +198,21 @@ type SlackAttachmentField struct { Short SlackCompatibleBool `json:"short"` } +func (s *SlackAttachmentField) IsValid() error { + var multiErr *multierror.Error + + if s.Value != nil { + switch s.Value.(type) { + case string, int: + // Valid types + default: + multiErr = multierror.Append(multiErr, fmt.Errorf("value must be either a string or int")) + } + } + + return multiErr.ErrorOrNil() +} + func (s *SlackAttachmentField) Equals(input *SlackAttachmentField) bool { if s.Title != input.Title { return false @@ -188,7 +281,7 @@ func ParseSlackAttachment(post *Post, attachments []*SlackAttachment) { } postAttachments = append(postAttachments, attachment) } - post.AddProp("attachments", postAttachments) + post.AddProp(PostPropsAttachments, postAttachments) } func ParseSlackLinksToMarkdown(text string) string { diff --git a/server/public/model/slack_attachment_test.go b/server/public/model/slack_attachment_test.go index 2ccc05689b..bca19bc7d7 100644 --- a/server/public/model/slack_attachment_test.go +++ b/server/public/model/slack_attachment_test.go @@ -9,6 +9,165 @@ import ( "github.com/stretchr/testify/assert" ) +func TestSlackAttachment_IsValid(t *testing.T) { + tests := map[string]struct { + attachment *SlackAttachment + wantErr string + }{ + "valid attachment": { + attachment: &SlackAttachment{ + Text: "This is a test", + }, + wantErr: "", + }, + "invalid color": { + attachment: &SlackAttachment{ + Color: "invalid", + }, + wantErr: "invalid style 'invalid' - must be one of [good, warning, danger] or a hex color", + }, + "valid predefined color": { + attachment: &SlackAttachment{ + Color: "good", + }, + wantErr: "", + }, + "valid warning color": { + attachment: &SlackAttachment{ + Color: "warning", + }, + wantErr: "", + }, + "valid danger color": { + attachment: &SlackAttachment{ + Color: "danger", + }, + wantErr: "", + }, + "valid hex color": { + attachment: &SlackAttachment{ + Color: "#FF0000", + }, + wantErr: "", + }, + "author link without name": { + attachment: &SlackAttachment{ + AuthorLink: "http://example.com", + }, + wantErr: "author link cannot be set without author name", + }, + "invalid author link": { + attachment: &SlackAttachment{ + AuthorName: "Author", + AuthorLink: "invalid-url", + }, + wantErr: "invalid author link URL", + }, + "invalid author icon": { + attachment: &SlackAttachment{ + AuthorIcon: "invalid-url", + }, + wantErr: "invalid author icon URL", + }, + "title link without title": { + attachment: &SlackAttachment{ + TitleLink: "http://example.com", + }, + wantErr: "title link cannot be set without title", + }, + "invalid title link": { + attachment: &SlackAttachment{ + Title: "Title", + TitleLink: "invalid-url", + }, + wantErr: "invalid title link URL", + }, + "invalid image URL": { + attachment: &SlackAttachment{ + ImageURL: "invalid-url", + }, + wantErr: "invalid image URL", + }, + "invalid thumb URL": { + attachment: &SlackAttachment{ + ThumbURL: "invalid-url", + }, + wantErr: "invalid thumb URL", + }, + "invalid footer icon": { + attachment: &SlackAttachment{ + FooterIcon: "invalid-url", + }, + wantErr: "invalid footer icon URL", + }, + "invalid timestamp type": { + attachment: &SlackAttachment{ + Timestamp: []string{"invalid"}, + }, + wantErr: "timestamp must be either a string or int64", + }, + "valid timestamp string": { + attachment: &SlackAttachment{ + Timestamp: "1234567890", + }, + wantErr: "", + }, + "valid timestamp int64": { + attachment: &SlackAttachment{ + Timestamp: int64(1234567890), + }, + wantErr: "", + }, + "invalid action": { + attachment: &SlackAttachment{ + Actions: []*PostAction{ + { + Name: "", // Invalid - missing name + }, + }, + }, + wantErr: "action must have a name", + }, + "invalid field value type": { + attachment: &SlackAttachment{ + Fields: []*SlackAttachmentField{ + { + Title: "Title", + Value: []string{"invalid"}, + }, + }, + }, + wantErr: "value must be either a string or int", + }, + "valid fields": { + attachment: &SlackAttachment{ + Fields: []*SlackAttachmentField{ + { + Title: "Title", + Value: "string value", + }, + { + Title: "Number", + Value: 42, + }, + }, + }, + wantErr: "", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + err := tc.attachment.IsValid() + if tc.wantErr == "" { + assert.NoError(t, err, name) + } else { + assert.ErrorContains(t, err, tc.wantErr, name) + } + }) + } +} + func TestParseSlackAttachment(t *testing.T) { t.Run("empty list", func(t *testing.T) { post := &Post{} @@ -19,7 +178,7 @@ func TestParseSlackAttachment(t *testing.T) { expectedPost := &Post{ Type: PostTypeSlackAttachment, Props: map[string]any{ - "attachments": []*SlackAttachment{}, + PostPropsAttachments: []*SlackAttachment{}, }, } assert.Equal(t, expectedPost, post) @@ -36,7 +195,7 @@ func TestParseSlackAttachment(t *testing.T) { expectedPost := &Post{ Type: PostTypeSlackAttachment, Props: map[string]any{ - "attachments": []*SlackAttachment{}, + PostPropsAttachments: []*SlackAttachment{}, }, } assert.Equal(t, expectedPost, post) diff --git a/server/public/pluginapi/experimental/bot/poster/default_poster_test.go b/server/public/pluginapi/experimental/bot/poster/default_poster_test.go index 2770b13f88..9daa75f161 100644 --- a/server/public/pluginapi/experimental/bot/poster/default_poster_test.go +++ b/server/public/pluginapi/experimental/bot/poster/default_poster_test.go @@ -106,7 +106,7 @@ func TestDMWithAttachments(t *testing.T) { ChannelId: dmChannelID, Type: model.PostTypeSlackAttachment, Props: model.StringInterface{ - "attachments": attachments, + model.PostPropsAttachments: attachments, }, } diff --git a/server/public/pluginapi/experimental/flow/handler.go b/server/public/pluginapi/experimental/flow/handler.go index c3444fe754..db2fac441c 100644 --- a/server/public/pluginapi/experimental/flow/handler.go +++ b/server/public/pluginapi/experimental/flow/handler.go @@ -196,7 +196,7 @@ func (f *Flow) handle( } func (f *Flow) processButtonPostActions(post *model.Post) { - attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) if !ok || len(attachments) == 0 { return } diff --git a/server/public/pluginapi/experimental/flow/step.go b/server/public/pluginapi/experimental/flow/step.go index 3f5d32e77c..f879d8c6e0 100644 --- a/server/public/pluginapi/experimental/flow/step.go +++ b/server/public/pluginapi/experimental/flow/step.go @@ -145,7 +145,7 @@ func (s Step) render(f *Flow, done bool, selectedButton int) (*model.Post, bool, buttons := processButtons(s.buttons, f.state.AppState) - attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment) if !ok || len(attachments) != 1 { return nil, false, errors.New("expected 1 slack attachment") } @@ -224,6 +224,7 @@ func processDialog(in *model.Dialog, state State) model.Dialog { func renderButton(b Button, stepName Name, i int, state State) *model.PostAction { return &model.PostAction{ + Type: model.PostActionTypeButton, Name: formatState(b.Name, state), Disabled: b.Disabled, Style: string(b.Color), diff --git a/server/public/pluginapi/experimental/panel/settings/bool_setting.go b/server/public/pluginapi/experimental/panel/settings/bool_setting.go index bb99d35287..0d7d9e9c36 100644 --- a/server/public/pluginapi/experimental/panel/settings/bool_setting.go +++ b/server/public/pluginapi/experimental/panel/settings/bool_setting.go @@ -75,6 +75,7 @@ func (s *boolSetting) GetSlackAttachments(userID, settingHandler string, disable currentValueMessage = fmt.Sprintf("Current value: %s", currentTextValue) actionTrue := model.PostAction{ + Type: model.PostActionTypeButton, Name: "Yes", Integration: &model.PostActionIntegration{ URL: settingHandler, @@ -86,6 +87,7 @@ func (s *boolSetting) GetSlackAttachments(userID, settingHandler string, disable } actionFalse := model.PostAction{ + Type: model.PostActionTypeButton, Name: "No", Integration: &model.PostActionIntegration{ URL: settingHandler, diff --git a/server/public/pluginapi/experimental/panel/settings/option_setting.go b/server/public/pluginapi/experimental/panel/settings/option_setting.go index 5d90878641..84dfcaf012 100644 --- a/server/public/pluginapi/experimental/panel/settings/option_setting.go +++ b/server/public/pluginapi/experimental/panel/settings/option_setting.go @@ -62,6 +62,7 @@ func (s *optionSetting) GetSlackAttachments(userID, settingHandler string, disab currentValueMessage = fmt.Sprintf("Current value: %s", currentTextValue) actionOptions := model.PostAction{ + Type: model.PostActionTypeSelect, Name: "Select an option:", Integration: &model.PostActionIntegration{ URL: settingHandler + "?" + s.id + "=true", @@ -69,7 +70,6 @@ func (s *optionSetting) GetSlackAttachments(userID, settingHandler string, disab ContextIDKey: s.id, }, }, - Type: "select", Options: stringsToOptions(s.options), } diff --git a/server/public/pluginapi/post.go b/server/public/pluginapi/post.go index e4328fe4dd..fdeb10bed2 100644 --- a/server/public/pluginapi/post.go +++ b/server/public/pluginapi/post.go @@ -297,7 +297,7 @@ func (p *PostService) ShouldProcessMessage(post *model.Post, options ...ShouldPr return false, nil } - if !messageProcessOptions.AllowWebhook && post.GetProp("from_webhook") == "true" { + if !messageProcessOptions.AllowWebhook && post.GetProp(model.PostPropsFromWebhook) == "true" { return false, nil } diff --git a/server/public/pluginapi/post_test.go b/server/public/pluginapi/post_test.go index 85eb693f9e..310ee10384 100644 --- a/server/public/pluginapi/post_test.go +++ b/server/public/pluginapi/post_test.go @@ -660,7 +660,7 @@ func TestShouldProcessMessage(t *testing.T) { client := pluginapi.NewClient(api, &plugintest.Driver{}) shouldProcessMessage, err := client.Post.ShouldProcessMessage( - &model.Post{ChannelId: channelID, Props: model.StringInterface{"from_webhook": "true"}}, + &model.Post{ChannelId: channelID, Props: model.StringInterface{model.PostPropsFromWebhook: "true"}}, pluginapi.AllowBots(), ) @@ -677,7 +677,7 @@ func TestShouldProcessMessage(t *testing.T) { client := pluginapi.NewClient(api, &plugintest.Driver{}) shouldProcessMessage, err := client.Post.ShouldProcessMessage( - &model.Post{ChannelId: channelID, Props: model.StringInterface{"from_webhook": "true"}}, + &model.Post{ChannelId: channelID, Props: model.StringInterface{model.PostPropsFromWebhook: "true"}}, pluginapi.AllowBots(), pluginapi.AllowWebhook(), ) @@ -712,7 +712,7 @@ func TestShouldProcessMessage(t *testing.T) { client := pluginapi.NewClient(api, &plugintest.Driver{}) shouldProcessMessage, err := client.Post.ShouldProcessMessage( - &model.Post{ChannelId: channelID, Props: model.StringInterface{"from_webhook": "false"}}, + &model.Post{ChannelId: channelID, Props: model.StringInterface{model.PostPropsFromWebhook: "false"}}, pluginapi.AllowBots(), )