diff options
95 files changed, 3452 insertions, 1316 deletions
@@ -275,6 +275,14 @@ gofmt: ## Runs gofmt against all packages. done @echo "gofmt success"; \ +megacheck: ## Run megacheck on codebasis + go get honnef.co/go/tools/cmd/megacheck + $(GOPATH)/bin/megacheck $(TE_PACKAGES) + +ifeq ($(BUILD_ENTERPRISE_READY),true) + $(GOPATH)/bin/megacheck $(EE_PACKAGES) || exit 1 +endif + store-mocks: ## Creates mock files. go get -u github.com/vektra/mockery/... $(GOPATH)/bin/mockery -dir store -all -output store/storetest/mocks -note 'Regenerate this file using `make store-mocks`.' diff --git a/api4/channel.go b/api4/channel.go index d497c9793..b29d2a94c 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -22,6 +22,7 @@ func (api *API) InitChannel() { api.BaseRoutes.ChannelsForTeam.Handle("/ids", api.ApiSessionRequired(getPublicChannelsByIdsForTeam)).Methods("POST") api.BaseRoutes.ChannelsForTeam.Handle("/search", api.ApiSessionRequired(searchChannelsForTeam)).Methods("POST") api.BaseRoutes.ChannelsForTeam.Handle("/autocomplete", api.ApiSessionRequired(autocompleteChannelsForTeam)).Methods("GET") + api.BaseRoutes.ChannelsForTeam.Handle("/search_autocomplete", api.ApiSessionRequired(autocompleteChannelsForTeamForSearch)).Methods("GET") api.BaseRoutes.User.Handle("/teams/{team_id:[A-Za-z0-9]+}/channels", api.ApiSessionRequired(getChannelsForTeamForUser)).Methods("GET") api.BaseRoutes.Channel.Handle("", api.ApiSessionRequired(getChannel)).Methods("GET") @@ -642,6 +643,30 @@ func autocompleteChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Requ w.Write([]byte(channels.ToJson())) } +func autocompleteChannelsForTeamForSearch(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireTeamId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToTeam(c.Session, c.Params.TeamId, model.PERMISSION_LIST_TEAM_CHANNELS) { + c.SetPermissionError(model.PERMISSION_LIST_TEAM_CHANNELS) + return + } + + name := r.URL.Query().Get("name") + + channels, err := c.App.AutocompleteChannelsForSearch(c.Params.TeamId, c.Session.UserId, name) + if err != nil { + c.Err = err + return + } + + // Don't fill in channels props, since unused by client and potentially expensive. + + w.Write([]byte(channels.ToJson())) +} + func searchChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) { c.RequireTeamId() if c.Err != nil { diff --git a/api4/channel_test.go b/api4/channel_test.go index 8593ea831..2aec90aea 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -150,8 +150,8 @@ func TestUpdateChannel(t *testing.T) { channel := &model.Channel{DisplayName: "Test API Name", Name: GenerateTestChannelName(), Type: model.CHANNEL_OPEN, TeamId: team.Id} private := &model.Channel{DisplayName: "Test API Name", Name: GenerateTestChannelName(), Type: model.CHANNEL_PRIVATE, TeamId: team.Id} - channel, resp := Client.CreateChannel(channel) - private, resp = Client.CreateChannel(private) + channel, _ = Client.CreateChannel(channel) + private, _ = Client.CreateChannel(private) //Update a open channel channel.DisplayName = "My new display name" @@ -434,7 +434,7 @@ func TestCreateGroupChannel(t *testing.T) { t.Fatal("should be equal") } - rgc, resp = Client.CreateGroupChannel([]string{user2.Id}) + _, resp = Client.CreateGroupChannel([]string{user2.Id}) CheckBadRequestStatus(t, resp) user4 := th.CreateUser() @@ -541,12 +541,12 @@ func TestGetDeletedChannelsForTeam(t *testing.T) { Client := th.Client team := th.BasicTeam - channels, resp := Client.GetDeletedChannelsForTeam(team.Id, 0, 100, "") + _, resp := Client.GetDeletedChannelsForTeam(team.Id, 0, 100, "") CheckForbiddenStatus(t, resp) th.LoginTeamAdmin() - channels, resp = Client.GetDeletedChannelsForTeam(team.Id, 0, 100, "") + channels, resp := Client.GetDeletedChannelsForTeam(team.Id, 0, 100, "") CheckNoError(t, resp) numInitialChannelsForTeam := len(channels) @@ -860,6 +860,7 @@ func TestDeleteChannel(t *testing.T) { // successful delete of channel with multiple members publicChannel3 := th.CreatePublicChannel() + th.App.AddUserToChannel(user, publicChannel3) th.App.AddUserToChannel(user2, publicChannel3) _, resp = Client.DeleteChannel(publicChannel3.Id) CheckNoError(t, resp) @@ -901,7 +902,7 @@ func TestDeleteChannel(t *testing.T) { publicChannel5 := th.CreatePublicChannel() Client.Logout() - Client.Login(user2.Id, user2.Password) + Client.Login(user.Id, user.Password) _, resp = Client.DeleteChannel(publicChannel5.Id) CheckUnauthorizedStatus(t, resp) @@ -927,16 +928,14 @@ func TestDeleteChannel(t *testing.T) { th.AddPermissionToRole(model.PERMISSION_DELETE_PRIVATE_CHANNEL.Id, model.TEAM_USER_ROLE_ID) Client = th.Client - team = th.BasicTeam user = th.BasicUser - user2 = th.BasicUser2 // channels created by SystemAdmin publicChannel6 := th.CreateChannelWithClient(th.SystemAdminClient, model.CHANNEL_OPEN) privateChannel7 := th.CreateChannelWithClient(th.SystemAdminClient, model.CHANNEL_PRIVATE) th.App.AddUserToChannel(user, publicChannel6) th.App.AddUserToChannel(user, privateChannel7) - th.App.AddUserToChannel(user2, privateChannel7) + th.App.AddUserToChannel(user, privateChannel7) // successful delete by user _, resp = Client.DeleteChannel(publicChannel6.Id) @@ -956,7 +955,7 @@ func TestDeleteChannel(t *testing.T) { privateChannel7 = th.CreateChannelWithClient(th.SystemAdminClient, model.CHANNEL_PRIVATE) th.App.AddUserToChannel(user, publicChannel6) th.App.AddUserToChannel(user, privateChannel7) - th.App.AddUserToChannel(user2, privateChannel7) + th.App.AddUserToChannel(user, privateChannel7) // cannot delete by user _, resp = Client.DeleteChannel(publicChannel6.Id) @@ -1629,7 +1628,7 @@ func TestUpdateChannelRoles(t *testing.T) { CheckNoError(t, resp) // System Admin promotes User 1 - pass, resp = th.SystemAdminClient.UpdateChannelRoles(channel.Id, th.BasicUser.Id, CHANNEL_ADMIN) + _, resp = th.SystemAdminClient.UpdateChannelRoles(channel.Id, th.BasicUser.Id, CHANNEL_ADMIN) CheckNoError(t, resp) th.LoginBasic() diff --git a/api4/command_test.go b/api4/command_test.go index 96025c063..10ffbc695 100644 --- a/api4/command_test.go +++ b/api4/command_test.go @@ -491,6 +491,7 @@ func TestExecuteGetCommand(t *testing.T) { require.Equal(t, token, values.Get("token")) require.Equal(t, th.BasicTeam.Name, values.Get("team_domain")) + require.Equal(t, "ourCommand", values.Get("cmd")) w.Header().Set("Content-Type", "application/json") w.Write([]byte(expectedCommandResponse.ToJson())) @@ -500,7 +501,7 @@ func TestExecuteGetCommand(t *testing.T) { getCmd := &model.Command{ CreatorId: th.BasicUser.Id, TeamId: th.BasicTeam.Id, - URL: ts.URL, + URL: ts.URL + "/?cmd=ourCommand", Method: model.COMMAND_METHOD_GET, Trigger: "getcommand", Token: token, diff --git a/api4/emoji_test.go b/api4/emoji_test.go index 34fa602cc..e3aca4497 100644 --- a/api4/emoji_test.go +++ b/api4/emoji_test.go @@ -340,12 +340,16 @@ func TestDeleteEmoji(t *testing.T) { th.RemovePermissionFromRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) + Client.Logout() th.LoginBasic2() - ok, resp = Client.DeleteEmoji(newEmoji.Id) + + _, resp = Client.DeleteEmoji(newEmoji.Id) CheckForbiddenStatus(t, resp) + th.RemovePermissionFromRole(model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) th.AddPermissionToRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) + Client.Logout() th.LoginBasic() @@ -360,8 +364,10 @@ func TestDeleteEmoji(t *testing.T) { Client.Logout() th.LoginBasic2() - ok, resp = Client.DeleteEmoji(newEmoji.Id) + + _, resp = Client.DeleteEmoji(newEmoji.Id) CheckForbiddenStatus(t, resp) + Client.Logout() th.LoginBasic() @@ -376,9 +382,11 @@ func TestDeleteEmoji(t *testing.T) { th.AddPermissionToRole(model.PERMISSION_MANAGE_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_EMOJIS.Id, model.SYSTEM_USER_ROLE_ID) + Client.Logout() th.LoginBasic2() - ok, resp = Client.DeleteEmoji(newEmoji.Id) + + _, resp = Client.DeleteEmoji(newEmoji.Id) CheckNoError(t, resp) Client.Logout() @@ -412,7 +420,8 @@ func TestDeleteEmoji(t *testing.T) { Client.Logout() th.LoginBasic2() - ok, resp = Client.DeleteEmoji(newEmoji.Id) + + _, resp = Client.DeleteEmoji(newEmoji.Id) CheckNoError(t, resp) } diff --git a/api4/file_test.go b/api4/file_test.go index 9143f4839..607e54dd7 100644 --- a/api4/file_test.go +++ b/api4/file_test.go @@ -446,7 +446,7 @@ func TestGetFileLink(t *testing.T) { fileId = fileResp.FileInfos[0].Id } - link, resp := Client.GetFileLink(fileId) + _, resp := Client.GetFileLink(fileId) CheckBadRequestStatus(t, resp) // Hacky way to assign file to a post (usually would be done by CreatePost call) @@ -460,8 +460,9 @@ func TestGetFileLink(t *testing.T) { time.Sleep(2 * time.Second) th.App.UpdateConfig(func(cfg *model.Config) { cfg.FileSettings.EnablePublicLink = true }) - link, resp = Client.GetFileLink(fileId) + link, resp := Client.GetFileLink(fileId) CheckNoError(t, resp) + if link == "" { t.Fatal("should've received public link") } diff --git a/api4/oauth.go b/api4/oauth.go index 961b0fecd..990f292e9 100644 --- a/api4/oauth.go +++ b/api4/oauth.go @@ -452,6 +452,15 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { service := c.Params.Service + oauthError := r.URL.Query().Get("error") + if oauthError == "access_denied" { + utils.RenderWebError(c.App.Config(), w, r, http.StatusTemporaryRedirect, url.Values{ + "type": []string{"oauth_access_denied"}, + "service": []string{strings.Title(service)}, + }, c.App.AsymmetricSigningKey()) + return + } + code := r.URL.Query().Get("code") if len(code) == 0 { utils.RenderWebError(c.App.Config(), w, r, http.StatusTemporaryRedirect, url.Values{ diff --git a/api4/oauth_test.go b/api4/oauth_test.go index cac40e442..dcc7cc5a2 100644 --- a/api4/oauth_test.go +++ b/api4/oauth_test.go @@ -8,6 +8,7 @@ import ( "io" "io/ioutil" "net/http" + "net/http/httptest" "net/url" "strconv" "testing" @@ -18,6 +19,7 @@ import ( "github.com/mattermost/mattermost-server/einterfaces" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" + "github.com/mattermost/mattermost-server/web" ) func TestCreateOAuthApp(t *testing.T) { @@ -1147,6 +1149,30 @@ func TestOAuthComplete(t *testing.T) { } } +func TestOAuthComplete_AccessDenied(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + c := &Context{ + App: th.App, + Params: &web.Params{ + Service: "TestService", + }, + } + responseWriter := httptest.NewRecorder() + request, _ := http.NewRequest(http.MethodGet, th.App.GetSiteURL()+"/signup/TestService/complete?error=access_denied", nil) + + completeOAuth(c, responseWriter, request) + + response := responseWriter.Result() + + assert.Equal(t, http.StatusTemporaryRedirect, response.StatusCode) + + location, _ := url.Parse(response.Header.Get("Location")) + assert.Equal(t, "oauth_access_denied", location.Query().Get("type")) + assert.Equal(t, "TestService", location.Query().Get("service")) +} + func HttpGet(url string, httpClient *http.Client, authToken string, followRedirect bool) (*http.Response, *model.AppError) { rq, _ := http.NewRequest("GET", url, nil) rq.Close = true diff --git a/api4/post_test.go b/api4/post_test.go index c428d3ab2..8ccd88a42 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -65,7 +65,7 @@ func TestCreatePost(t *testing.T) { CheckBadRequestStatus(t, resp) post2 := &model.Post{ChannelId: th.BasicChannel2.Id, Message: "zz" + model.NewId() + "a", CreateAt: 123} - rpost2, resp := Client.CreatePost(post2) + rpost2, _ := Client.CreatePost(post2) if rpost2.CreateAt == post2.CreateAt { t.Fatal("create at should not match") @@ -154,7 +154,7 @@ func TestCreatePostEphemeral(t *testing.T) { CheckUnauthorizedStatus(t, resp) Client = th.Client - rpost, resp = Client.CreatePostEphemeral(ephemeralPost) + _, resp = Client.CreatePostEphemeral(ephemeralPost) CheckForbiddenStatus(t, resp) } @@ -1023,22 +1023,22 @@ func TestGetFlaggedPostsForUser(t *testing.T) { Client.Logout() - rpl, resp = Client.GetFlaggedPostsForUserInChannel(user.Id, channel1.Id, 0, 10) + _, resp = Client.GetFlaggedPostsForUserInChannel(user.Id, channel1.Id, 0, 10) CheckUnauthorizedStatus(t, resp) - rpl, resp = Client.GetFlaggedPostsForUserInTeam(user.Id, team1.Id, 0, 10) + _, resp = Client.GetFlaggedPostsForUserInTeam(user.Id, team1.Id, 0, 10) CheckUnauthorizedStatus(t, resp) - rpl, resp = Client.GetFlaggedPostsForUser(user.Id, 0, 10) + _, resp = Client.GetFlaggedPostsForUser(user.Id, 0, 10) CheckUnauthorizedStatus(t, resp) - rpl, resp = th.SystemAdminClient.GetFlaggedPostsForUserInChannel(user.Id, channel1.Id, 0, 10) + _, resp = th.SystemAdminClient.GetFlaggedPostsForUserInChannel(user.Id, channel1.Id, 0, 10) CheckNoError(t, resp) - rpl, resp = th.SystemAdminClient.GetFlaggedPostsForUserInTeam(user.Id, team1.Id, 0, 10) + _, resp = th.SystemAdminClient.GetFlaggedPostsForUserInTeam(user.Id, team1.Id, 0, 10) CheckNoError(t, resp) - rpl, resp = th.SystemAdminClient.GetFlaggedPostsForUser(user.Id, 0, 10) + _, resp = th.SystemAdminClient.GetFlaggedPostsForUser(user.Id, 0, 10) CheckNoError(t, resp) } @@ -1153,25 +1153,25 @@ func TestGetPost(t *testing.T) { Client.RemoveUserFromChannel(th.BasicChannel.Id, th.BasicUser.Id) // Channel is public, should be able to read post - post, resp = Client.GetPost(th.BasicPost.Id, "") + _, resp = Client.GetPost(th.BasicPost.Id, "") CheckNoError(t, resp) privatePost := th.CreatePostWithClient(Client, th.BasicPrivateChannel) - post, resp = Client.GetPost(privatePost.Id, "") + _, resp = Client.GetPost(privatePost.Id, "") CheckNoError(t, resp) Client.RemoveUserFromChannel(th.BasicPrivateChannel.Id, th.BasicUser.Id) // Channel is private, should not be able to read post - post, resp = Client.GetPost(privatePost.Id, "") + _, resp = Client.GetPost(privatePost.Id, "") CheckForbiddenStatus(t, resp) Client.Logout() _, resp = Client.GetPost(model.NewId(), "") CheckUnauthorizedStatus(t, resp) - post, resp = th.SystemAdminClient.GetPost(th.BasicPost.Id, "") + _, resp = th.SystemAdminClient.GetPost(th.BasicPost.Id, "") CheckNoError(t, resp) } @@ -1267,7 +1267,7 @@ func TestGetPostThread(t *testing.T) { _, resp = Client.GetPostThread(model.NewId(), "") CheckUnauthorizedStatus(t, resp) - list, resp = th.SystemAdminClient.GetPostThread(th.BasicPost.Id, "") + _, resp = th.SystemAdminClient.GetPostThread(th.BasicPost.Id, "") CheckNoError(t, resp) } @@ -1351,12 +1351,13 @@ func TestSearchPosts(t *testing.T) { t.Fatal("wrong search") } - if posts, resp = Client.SearchPosts(th.BasicTeam.Id, "*", false); len(posts.Order) != 0 { + if posts, _ = Client.SearchPosts(th.BasicTeam.Id, "*", false); len(posts.Order) != 0 { t.Fatal("searching for just * shouldn't return any results") } posts, resp = Client.SearchPosts(th.BasicTeam.Id, "post1 post2", true) CheckNoError(t, resp) + if len(posts.Order) != 2 { t.Fatal("wrong search results") } diff --git a/api4/preference_test.go b/api4/preference_test.go index 68d34784f..41681ff69 100644 --- a/api4/preference_test.go +++ b/api4/preference_test.go @@ -105,15 +105,15 @@ func TestGetPreferencesByCategory(t *testing.T) { t.Fatalf("received the wrong number of preferences %v:%v", len(prefs), 2) } - prefs, resp = Client.GetPreferencesByCategory(user1.Id, "junk") + _, resp = Client.GetPreferencesByCategory(user1.Id, "junk") CheckNotFoundStatus(t, resp) th.LoginBasic2() - prefs, resp = Client.GetPreferencesByCategory(th.BasicUser2.Id, category) + _, resp = Client.GetPreferencesByCategory(th.BasicUser2.Id, category) CheckNotFoundStatus(t, resp) - prefs, resp = Client.GetPreferencesByCategory(user1.Id, category) + _, resp = Client.GetPreferencesByCategory(user1.Id, category) CheckForbiddenStatus(t, resp) prefs, resp = Client.GetPreferencesByCategory(th.BasicUser2.Id, "junk") @@ -309,7 +309,7 @@ func TestDeletePreferences(t *testing.T) { th.LoginBasic() - prefs, resp := Client.GetPreferences(th.BasicUser.Id) + prefs, _ := Client.GetPreferences(th.BasicUser.Id) originalCount := len(prefs) // save 10 preferences @@ -328,7 +328,7 @@ func TestDeletePreferences(t *testing.T) { // delete 10 preferences th.LoginBasic2() - _, resp = Client.DeletePreferences(th.BasicUser2.Id, &preferences) + _, resp := Client.DeletePreferences(th.BasicUser2.Id, &preferences) CheckForbiddenStatus(t, resp) th.LoginBasic() @@ -339,7 +339,7 @@ func TestDeletePreferences(t *testing.T) { _, resp = Client.DeletePreferences(th.BasicUser2.Id, &preferences) CheckForbiddenStatus(t, resp) - prefs, resp = Client.GetPreferences(th.BasicUser.Id) + prefs, _ = Client.GetPreferences(th.BasicUser.Id) if len(prefs) != originalCount { t.Fatal("should've deleted preferences") } diff --git a/api4/role_test.go b/api4/role_test.go index 8149ff3c6..2a8008dc9 100644 --- a/api4/role_test.go +++ b/api4/role_test.go @@ -130,7 +130,7 @@ func TestGetRolesByNames(t *testing.T) { assert.Contains(t, received, role3) // Check a list of non-existent roles. - received, resp = th.Client.GetRolesByNames([]string{model.NewId(), model.NewId()}) + _, resp = th.Client.GetRolesByNames([]string{model.NewId(), model.NewId()}) CheckNoError(t, resp) // Empty list should error. @@ -138,11 +138,11 @@ func TestGetRolesByNames(t *testing.T) { CheckBadRequestStatus(t, resp) // Invalid role name should error. - received, resp = th.Client.GetRolesByNames([]string{model.NewId(), model.NewId(), "!!!!!!"}) + _, resp = th.Client.GetRolesByNames([]string{model.NewId(), model.NewId(), "!!!!!!"}) CheckBadRequestStatus(t, resp) // Empty/whitespace rolenames should be ignored. - received, resp = th.Client.GetRolesByNames([]string{model.NewId(), model.NewId(), "", " "}) + _, resp = th.Client.GetRolesByNames([]string{model.NewId(), model.NewId(), "", " "}) CheckNoError(t, resp) } @@ -178,16 +178,16 @@ func TestPatchRole(t *testing.T) { assert.Equal(t, received.SchemeManaged, role.SchemeManaged) // Check a no-op patch succeeds. - received, resp = th.SystemAdminClient.PatchRole(role.Id, patch) + _, resp = th.SystemAdminClient.PatchRole(role.Id, patch) CheckNoError(t, resp) - received, resp = th.SystemAdminClient.PatchRole("junk", patch) + _, resp = th.SystemAdminClient.PatchRole("junk", patch) CheckBadRequestStatus(t, resp) - received, resp = th.Client.PatchRole(model.NewId(), patch) + _, resp = th.Client.PatchRole(model.NewId(), patch) CheckNotFoundStatus(t, resp) - received, resp = th.Client.PatchRole(role.Id, patch) + _, resp = th.Client.PatchRole(role.Id, patch) CheckForbiddenStatus(t, resp) // Check a change that the license would not allow. @@ -195,7 +195,7 @@ func TestPatchRole(t *testing.T) { Permissions: &[]string{"manage_system", "manage_webhooks"}, } - received, resp = th.SystemAdminClient.PatchRole(role.Id, patch) + _, resp = th.SystemAdminClient.PatchRole(role.Id, patch) CheckNotImplementedStatus(t, resp) // Add a license. diff --git a/api4/status_test.go b/api4/status_test.go index 9b3583c1e..afff8526d 100644 --- a/api4/status_test.go +++ b/api4/status_test.go @@ -150,11 +150,11 @@ func TestUpdateUserStatus(t *testing.T) { } toUpdateUserStatus.Status = "online" - updateUserStatus, resp = Client.UpdateUserStatus(th.BasicUser2.Id, toUpdateUserStatus) + _, resp = Client.UpdateUserStatus(th.BasicUser2.Id, toUpdateUserStatus) CheckForbiddenStatus(t, resp) toUpdateUserStatus.Status = "online" - updateUserStatus, resp = th.SystemAdminClient.UpdateUserStatus(th.BasicUser2.Id, toUpdateUserStatus) + updateUserStatus, _ = th.SystemAdminClient.UpdateUserStatus(th.BasicUser2.Id, toUpdateUserStatus) if updateUserStatus.Status != "online" { t.Fatal("Should return online status") } diff --git a/api4/team_test.go b/api4/team_test.go index 468b9451d..8304c979d 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -858,10 +858,10 @@ func TestSearchAllTeams(t *testing.T) { Client.Logout() - rteams, resp = Client.SearchTeams(&model.TeamSearch{Term: pTeam.Name}) + _, resp = Client.SearchTeams(&model.TeamSearch{Term: pTeam.Name}) CheckUnauthorizedStatus(t, resp) - rteams, resp = Client.SearchTeams(&model.TeamSearch{Term: pTeam.DisplayName}) + _, resp = Client.SearchTeams(&model.TeamSearch{Term: pTeam.DisplayName}) CheckUnauthorizedStatus(t, resp) } @@ -1148,10 +1148,10 @@ func TestGetTeamMembers(t *testing.T) { CheckForbiddenStatus(t, resp) Client.Logout() - rmembers, resp = Client.GetTeamMembers(team.Id, 0, 1, "") + _, resp = Client.GetTeamMembers(team.Id, 0, 1, "") CheckUnauthorizedStatus(t, resp) - rmembers, resp = th.SystemAdminClient.GetTeamMembers(team.Id, 0, 100, "") + _, resp = th.SystemAdminClient.GetTeamMembers(team.Id, 0, 100, "") CheckNoError(t, resp) } @@ -1220,10 +1220,10 @@ func TestGetTeamMembersByIds(t *testing.T) { t.Fatal("1 user should be returned") } - tm1, resp = Client.GetTeamMembersByIds("junk", []string{th.BasicUser.Id}) + _, resp = Client.GetTeamMembersByIds("junk", []string{th.BasicUser.Id}) CheckBadRequestStatus(t, resp) - tm1, resp = Client.GetTeamMembersByIds(model.NewId(), []string{th.BasicUser.Id}) + _, resp = Client.GetTeamMembersByIds(model.NewId(), []string{th.BasicUser.Id}) CheckForbiddenStatus(t, resp) Client.Logout() @@ -1244,7 +1244,7 @@ func TestAddTeamMember(t *testing.T) { // Regular user can't add a member to a team they don't belong to. th.LoginBasic2() - tm, resp := Client.AddTeamMember(team.Id, otherUser.Id) + _, resp := Client.AddTeamMember(team.Id, otherUser.Id) CheckForbiddenStatus(t, resp) if resp.Error == nil { t.Fatalf("Error is nil") @@ -1253,7 +1253,7 @@ func TestAddTeamMember(t *testing.T) { // Regular user can add a member to a team they belong to. th.LoginBasic() - tm, resp = Client.AddTeamMember(team.Id, otherUser.Id) + tm, resp := Client.AddTeamMember(team.Id, otherUser.Id) CheckNoError(t, resp) CheckCreatedStatus(t, resp) @@ -1370,7 +1370,7 @@ func TestAddTeamMember(t *testing.T) { token.CreateAt = model.GetMillis() - 1000*60*60*50 <-th.App.Srv.Store.Token().Save(token) - tm, resp = Client.AddTeamMemberFromInvite(token.Token, "") + _, resp = Client.AddTeamMemberFromInvite(token.Token, "") CheckBadRequestStatus(t, resp) th.App.DeleteToken(token) @@ -1382,7 +1382,7 @@ func TestAddTeamMember(t *testing.T) { ) <-th.App.Srv.Store.Token().Save(token) - tm, resp = Client.AddTeamMemberFromInvite(token.Token, "") + _, resp = Client.AddTeamMemberFromInvite(token.Token, "") CheckNotFoundStatus(t, resp) th.App.DeleteToken(token) @@ -1428,13 +1428,13 @@ func TestAddTeamMembers(t *testing.T) { // Regular user can't add a member to a team they don't belong to. th.LoginBasic2() - tm, resp := Client.AddTeamMembers(team.Id, userList) + _, resp := Client.AddTeamMembers(team.Id, userList) CheckForbiddenStatus(t, resp) Client.Logout() // Regular user can add a member to a team they belong to. th.LoginBasic() - tm, resp = Client.AddTeamMembers(team.Id, userList) + tm, resp := Client.AddTeamMembers(team.Id, userList) CheckNoError(t, resp) CheckCreatedStatus(t, resp) @@ -2018,7 +2018,7 @@ func TestGetTeamInviteInfo(t *testing.T) { team, resp = th.SystemAdminClient.UpdateTeam(team) CheckNoError(t, resp) - team, resp = Client.GetTeamInviteInfo(team.InviteId) + _, resp = Client.GetTeamInviteInfo(team.InviteId) CheckNoError(t, resp) _, resp = Client.GetTeamInviteInfo("junk") diff --git a/api4/user_test.go b/api4/user_test.go index 6cd64b7cf..e624d747d 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -406,7 +406,7 @@ func TestGetUser(t *testing.T) { CheckUnauthorizedStatus(t, resp) // System admins should ignore privacy settings - ruser, resp = th.SystemAdminClient.GetUser(user.Id, resp.Etag) + ruser, _ = th.SystemAdminClient.GetUser(user.Id, resp.Etag) if ruser.Email == "" { t.Fatal("email should not be blank") } @@ -474,7 +474,7 @@ func TestGetUserByUsername(t *testing.T) { CheckUnauthorizedStatus(t, resp) // System admins should ignore privacy settings - ruser, resp = th.SystemAdminClient.GetUserByUsername(user.Username, resp.Etag) + ruser, _ = th.SystemAdminClient.GetUserByUsername(user.Username, resp.Etag) if ruser.Email == "" { t.Fatal("email should not be blank") } @@ -539,7 +539,7 @@ func TestGetUserByEmail(t *testing.T) { CheckUnauthorizedStatus(t, resp) // System admins should ignore privacy settings - ruser, resp = th.SystemAdminClient.GetUserByEmail(user.Email, resp.Etag) + ruser, _ = th.SystemAdminClient.GetUserByEmail(user.Email, resp.Etag) if ruser.Email == "" { t.Fatal("email should not be blank") } @@ -2208,14 +2208,14 @@ func TestVerifyUserEmail(t *testing.T) { user := model.User{Email: th.GenerateTestEmail(), Nickname: "Darth Vader", Password: "hello1", Username: GenerateTestUsername(), Roles: model.SYSTEM_ADMIN_ROLE_ID + " " + model.SYSTEM_USER_ROLE_ID} - ruser, resp := Client.CreateUser(&user) + ruser, _ := Client.CreateUser(&user) token, err := th.App.CreateVerifyEmailToken(ruser.Id) if err != nil { t.Fatal("Unable to create email verify token") } - _, resp = Client.VerifyUserEmail(token.Token) + _, resp := Client.VerifyUserEmail(token.Token) CheckNoError(t, resp) _, resp = Client.VerifyUserEmail(GenerateTestId()) @@ -2329,7 +2329,7 @@ func TestCBALogin(t *testing.T) { } Client.HttpHeader["X-SSL-Client-Cert-Subject-DN"] = "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=" + th.BasicUser.Email - user, resp = Client.Login(th.BasicUser.Email, "") + user, _ = Client.Login(th.BasicUser.Email, "") if !(user != nil && user.Email == th.BasicUser.Email) { t.Fatal("Should have been able to login") } @@ -2340,13 +2340,13 @@ func TestCBALogin(t *testing.T) { }) Client.HttpHeader["X-SSL-Client-Cert-Subject-DN"] = "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=" + th.BasicUser.Email - user, resp = Client.Login(th.BasicUser.Email, "") + user, _ = Client.Login(th.BasicUser.Email, "") if resp.Error.StatusCode != 400 && user == nil { t.Fatal("Should have failed because password is required") } Client.HttpHeader["X-SSL-Client-Cert-Subject-DN"] = "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=" + th.BasicUser.Email - user, resp = Client.Login(th.BasicUser.Email, th.BasicUser.Password) + user, _ = Client.Login(th.BasicUser.Email, th.BasicUser.Password) if !(user != nil && user.Email == th.BasicUser.Email) { t.Fatal("Should have been able to login") } @@ -2597,7 +2597,7 @@ func TestGetUserAccessToken(t *testing.T) { _, resp = AdminClient.GetUserAccessToken(token.Id) CheckNoError(t, resp) - token, resp = Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) + _, resp = Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) CheckNoError(t, resp) rtokens, resp := Client.GetUserAccessTokensForUser(th.BasicUser.Id, 0, 100) diff --git a/api4/webhook_test.go b/api4/webhook_test.go index 764f25709..78598f9dc 100644 --- a/api4/webhook_test.go +++ b/api4/webhook_test.go @@ -365,7 +365,7 @@ func TestGetOutgoingWebhooks(t *testing.T) { t.Fatal("missing hook") } - hooks, resp = th.SystemAdminClient.GetOutgoingWebhooksForChannel(model.NewId(), 0, 1000, "") + _, resp = th.SystemAdminClient.GetOutgoingWebhooksForChannel(model.NewId(), 0, 1000, "") CheckForbiddenStatus(t, resp) _, resp = Client.GetOutgoingWebhooks(0, 1000, "") diff --git a/app/app.go b/app/app.go index b704bb449..511a464be 100644 --- a/app/app.go +++ b/app/app.go @@ -7,12 +7,10 @@ import ( "crypto/ecdsa" "fmt" "html/template" - "net" "net/http" "path" "reflect" "strconv" - "strings" "sync" "sync/atomic" @@ -100,6 +98,8 @@ type App struct { diagnosticId string phase2PermissionsMigrationComplete bool + + HTTPService HTTPService } var appCount = 0 @@ -125,6 +125,9 @@ func New(options ...Option) (outApp *App, outErr error) { clientConfig: make(map[string]string), licenseListeners: map[string]func(){}, } + + app.HTTPService = MakeHTTPService(app) + defer func() { if outErr != nil { app.Shutdown() @@ -209,6 +212,14 @@ func New(options ...Option) (outApp *App, outErr error) { } app.Srv.Store = app.newStore() + app.AddConfigListener(func(_, current *model.Config) { + if current.SqlSettings.EnablePublicChannelsMaterialization != nil && !*current.SqlSettings.EnablePublicChannelsMaterialization { + app.Srv.Store.Channel().DisableExperimentalPublicChannelsMaterialization() + } else { + app.Srv.Store.Channel().EnableExperimentalPublicChannelsMaterialization() + } + }) + if err := app.ensureAsymmetricSigningKey(); err != nil { return nil, errors.Wrapf(err, "unable to ensure asymmetric signing key") } @@ -285,6 +296,8 @@ func (a *App) Shutdown() { mlog.Info("Server stopped") a.DisableConfigWatch() + + a.HTTPService.Close() } var accountMigrationInterface func(*App) einterfaces.AccountMigrationInterface @@ -505,43 +518,6 @@ func (a *App) HTMLTemplates() *template.Template { return nil } -func (a *App) HTTPClient(trustURLs bool) *http.Client { - insecure := a.Config().ServiceSettings.EnableInsecureOutgoingConnections != nil && *a.Config().ServiceSettings.EnableInsecureOutgoingConnections - - if trustURLs { - return utils.NewHTTPClient(insecure, nil, nil) - } - - allowHost := func(host string) bool { - if a.Config().ServiceSettings.AllowedUntrustedInternalConnections == nil { - return false - } - for _, allowed := range strings.Fields(*a.Config().ServiceSettings.AllowedUntrustedInternalConnections) { - if host == allowed { - return true - } - } - return false - } - - allowIP := func(ip net.IP) bool { - if !utils.IsReservedIP(ip) { - return true - } - if a.Config().ServiceSettings.AllowedUntrustedInternalConnections == nil { - return false - } - for _, allowed := range strings.Fields(*a.Config().ServiceSettings.AllowedUntrustedInternalConnections) { - if _, ipRange, err := net.ParseCIDR(allowed); err == nil && ipRange.Contains(ip) { - return true - } - } - return false - } - - return utils.NewHTTPClient(insecure, allowHost, allowIP) -} - func (a *App) Handle404(w http.ResponseWriter, r *http.Request) { err := model.NewAppError("Handle404", "api.context.404.app_error", nil, "", http.StatusNotFound) @@ -642,7 +618,6 @@ func (a *App) DoEmojisPermissionsMigration() { mlog.Critical(err.Error()) return } - break case model.RESTRICT_EMOJI_CREATION_ADMIN: role, err = a.GetRoleByName(model.TEAM_ADMIN_ROLE_ID) if err != nil { @@ -650,10 +625,8 @@ func (a *App) DoEmojisPermissionsMigration() { mlog.Critical(err.Error()) return } - break case model.RESTRICT_EMOJI_CREATION_SYSTEM_ADMIN: role = nil - break default: mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.") mlog.Critical("Invalid restrict emoji creation setting") @@ -703,13 +676,13 @@ func (a *App) StartElasticsearch() { }) a.AddConfigListener(func(oldConfig *model.Config, newConfig *model.Config) { - if *oldConfig.ElasticsearchSettings.EnableIndexing == false && *newConfig.ElasticsearchSettings.EnableIndexing == true { + if !*oldConfig.ElasticsearchSettings.EnableIndexing && *newConfig.ElasticsearchSettings.EnableIndexing { a.Go(func() { if err := a.Elasticsearch.Start(); err != nil { mlog.Error(err.Error()) } }) - } else if *oldConfig.ElasticsearchSettings.EnableIndexing == true && *newConfig.ElasticsearchSettings.EnableIndexing == false { + } else if *oldConfig.ElasticsearchSettings.EnableIndexing && !*newConfig.ElasticsearchSettings.EnableIndexing { a.Go(func() { if err := a.Elasticsearch.Stop(); err != nil { mlog.Error(err.Error()) @@ -717,7 +690,7 @@ func (a *App) StartElasticsearch() { }) } else if *oldConfig.ElasticsearchSettings.Password != *newConfig.ElasticsearchSettings.Password || *oldConfig.ElasticsearchSettings.Username != *newConfig.ElasticsearchSettings.Username || *oldConfig.ElasticsearchSettings.ConnectionUrl != *newConfig.ElasticsearchSettings.ConnectionUrl || *oldConfig.ElasticsearchSettings.Sniff != *newConfig.ElasticsearchSettings.Sniff { a.Go(func() { - if *oldConfig.ElasticsearchSettings.EnableIndexing == true { + if *oldConfig.ElasticsearchSettings.EnableIndexing { if err := a.Elasticsearch.Stop(); err != nil { mlog.Error(err.Error()) } diff --git a/app/apptestlib.go b/app/apptestlib.go index 48783f49c..c0d2cfaa2 100644 --- a/app/apptestlib.go +++ b/app/apptestlib.go @@ -6,6 +6,8 @@ package app import ( "io" "io/ioutil" + "net/http" + "net/http/httptest" "os" "path/filepath" "time" @@ -33,6 +35,8 @@ type TestHelper struct { tempConfigPath string tempWorkspace string + + MockedHTTPService *MockedHTTPService } type persistentTestStore struct { @@ -163,6 +167,13 @@ func (me *TestHelper) InitSystemAdmin() *TestHelper { return me } +func (me *TestHelper) MockHTTPService(handler http.Handler) *TestHelper { + me.MockedHTTPService = MakeMockedHTTPService(handler) + me.App.HTTPService = me.MockedHTTPService + + return me +} + func (me *TestHelper) MakeEmail() string { return "success_" + model.NewId() + "@simulator.amazonses.com" } @@ -503,3 +514,22 @@ func (me *FakeClusterInterface) sendClearRoleCacheMessage() { Event: model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLES, }) } + +type MockedHTTPService struct { + Server *httptest.Server +} + +func MakeMockedHTTPService(handler http.Handler) *MockedHTTPService { + return &MockedHTTPService{ + Server: httptest.NewServer(handler), + } +} + +func (h *MockedHTTPService) MakeClient(trustURLs bool) *http.Client { + return h.Server.Client() +} + +func (h *MockedHTTPService) Close() { + h.Server.CloseClientConnections() + h.Server.Close() +} diff --git a/app/auto_responder_test.go b/app/auto_responder_test.go index f78bbc669..4afa03348 100644 --- a/app/auto_responder_test.go +++ b/app/auto_responder_test.go @@ -94,7 +94,7 @@ func TestSendAutoResponseSuccess(t *testing.T) { userUpdated1, err := th.App.PatchUser(user.Id, patch, true) require.Nil(t, err) - firstPost, err := th.App.CreatePost(&model.Post{ + firstPost, _ := th.App.CreatePost(&model.Post{ ChannelId: th.BasicChannel.Id, Message: "zz" + model.NewId() + "a", UserId: th.BasicUser.Id}, @@ -134,7 +134,7 @@ func TestSendAutoResponseFailure(t *testing.T) { userUpdated1, err := th.App.PatchUser(user.Id, patch, true) require.Nil(t, err) - firstPost, err := th.App.CreatePost(&model.Post{ + firstPost, _ := th.App.CreatePost(&model.Post{ ChannelId: th.BasicChannel.Id, Message: "zz" + model.NewId() + "a", UserId: th.BasicUser.Id}, diff --git a/app/brand.go b/app/brand.go index 4814264dd..f01393125 100644 --- a/app/brand.go +++ b/app/brand.go @@ -27,10 +27,10 @@ func (a *App) SaveBrandImage(imageData *multipart.FileHeader) *model.AppError { } file, err := imageData.Open() - defer file.Close() if err != nil { return model.NewAppError("SaveBrandImage", "brand.save_brand_image.open.app_error", nil, err.Error(), http.StatusBadRequest) } + defer file.Close() // Decode image config first to check dimensions before loading the whole thing into memory later on config, _, err := image.DecodeConfig(file) diff --git a/app/channel.go b/app/channel.go index 535eff724..4867908f9 100644 --- a/app/channel.go +++ b/app/channel.go @@ -52,7 +52,7 @@ func (a *App) JoinDefaultChannels(teamId string, user *model.User, shouldBeAdmin } else { seenChannels := map[string]bool{} for _, channelName := range a.Config().TeamSettings.ExperimentalDefaultChannels { - if seenChannels[channelName] != true { + if !seenChannels[channelName] { defaultChannelList = append(defaultChannelList, channelName) seenChannels[channelName] = true } @@ -1398,7 +1398,7 @@ func (a *App) removeUserFromChannel(userIdToRemove string, removerUserId string, var actorUser *model.User if removerUserId != "" { - actorUser, err = a.GetUser(removerUserId) + actorUser, _ = a.GetUser(removerUserId) } a.Go(func() { @@ -1507,6 +1507,16 @@ func (a *App) AutocompleteChannels(teamId string, term string) (*model.ChannelLi } } +func (a *App) AutocompleteChannelsForSearch(teamId string, userId string, term string) (*model.ChannelList, *model.AppError) { + includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels + + if result := <-a.Srv.Store.Channel().AutocompleteInTeamForSearch(teamId, userId, term, includeDeleted); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.ChannelList), nil + } +} + func (a *App) SearchChannels(teamId string, term string) (*model.ChannelList, *model.AppError) { includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels diff --git a/app/command.go b/app/command.go index 92c35865a..a1902bd10 100644 --- a/app/command.go +++ b/app/command.go @@ -233,7 +233,11 @@ func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, * var req *http.Request if cmd.Method == model.COMMAND_METHOD_GET { req, _ = http.NewRequest(http.MethodGet, cmd.URL, nil) - req.URL.RawQuery = p.Encode() + + if req.URL.RawQuery != "" { + req.URL.RawQuery += "&" + } + req.URL.RawQuery += p.Encode() } else { req, _ = http.NewRequest(http.MethodPost, cmd.URL, strings.NewReader(p.Encode())) } @@ -244,7 +248,7 @@ func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, * req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } - if resp, err := a.HTTPClient(false).Do(req); err != nil { + if resp, err := a.HTTPService.MakeClient(false).Do(req); err != nil { return nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, err.Error(), http.StatusInternalServerError) } else { if resp.StatusCode == http.StatusOK { diff --git a/app/command_channel_header.go b/app/command_channel_header.go index 100135f48..db92f68b2 100644 --- a/app/command_channel_header.go +++ b/app/command_channel_header.go @@ -4,9 +4,9 @@ package app import ( - "github.com/mattermost/mattermost-server/model" - goi18n "github.com/nicksnyder/go-i18n/i18n" + + "github.com/mattermost/mattermost-server/model" ) type HeaderProvider struct { @@ -37,33 +37,51 @@ func (me *HeaderProvider) GetCommand(a *App, T goi18n.TranslateFunc) *model.Comm func (me *HeaderProvider) DoCommand(a *App, args *model.CommandArgs, message string) *model.CommandResponse { channel, err := a.GetChannel(args.ChannelId) if err != nil { - return &model.CommandResponse{Text: args.T("api.command_channel_header.channel.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_channel_header.channel.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } switch channel.Type { case model.CHANNEL_OPEN: if !a.SessionHasPermissionToChannel(args.Session, args.ChannelId, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES) { - return &model.CommandResponse{Text: args.T("api.command_channel_header.permission.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_channel_header.permission.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } case model.CHANNEL_PRIVATE: if !a.SessionHasPermissionToChannel(args.Session, args.ChannelId, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES) { - return &model.CommandResponse{Text: args.T("api.command_channel_header.permission.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_channel_header.permission.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } case model.CHANNEL_GROUP, model.CHANNEL_DIRECT: // Modifying the header is not linked to any specific permission for group/dm channels, so just check for membership. channelMember, err := a.GetChannelMember(args.ChannelId, args.Session.UserId) if err != nil || channelMember == nil { - return &model.CommandResponse{Text: args.T("api.command_channel_header.permission.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_channel_header.permission.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } default: - return &model.CommandResponse{Text: args.T("api.command_channel_header.permission.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_channel_header.permission.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } if len(message) == 0 { - return &model.CommandResponse{Text: args.T("api.command_channel_header.message.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_channel_header.message.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } patch := &model.ChannelPatch{ @@ -73,7 +91,10 @@ func (me *HeaderProvider) DoCommand(a *App, args *model.CommandArgs, message str _, err = a.PatchChannel(channel, patch, args.UserId) if err != nil { - return &model.CommandResponse{Text: args.T("api.command_channel_header.update_channel.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_channel_header.update_channel.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } return &model.CommandResponse{} diff --git a/app/command_channel_header_test.go b/app/command_channel_header_test.go index 21735e044..99f3b0cc3 100644 --- a/app/command_channel_header_test.go +++ b/app/command_channel_header_test.go @@ -1,10 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + package app import ( "testing" - "github.com/mattermost/mattermost-server/model" "github.com/stretchr/testify/assert" + + "github.com/mattermost/mattermost-server/model" ) func TestHeaderProviderDoCommand(t *testing.T) { diff --git a/app/command_channel_purpose.go b/app/command_channel_purpose.go index 547406692..0ddbf1d64 100644 --- a/app/command_channel_purpose.go +++ b/app/command_channel_purpose.go @@ -4,8 +4,9 @@ package app import ( - "github.com/mattermost/mattermost-server/model" goi18n "github.com/nicksnyder/go-i18n/i18n" + + "github.com/mattermost/mattermost-server/model" ) type PurposeProvider struct { @@ -36,23 +37,39 @@ func (me *PurposeProvider) GetCommand(a *App, T goi18n.TranslateFunc) *model.Com func (me *PurposeProvider) DoCommand(a *App, args *model.CommandArgs, message string) *model.CommandResponse { channel, err := a.GetChannel(args.ChannelId) if err != nil { - return &model.CommandResponse{Text: args.T("api.command_channel_purpose.channel.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} - } - - if channel.Type == model.CHANNEL_OPEN && !a.SessionHasPermissionToChannel(args.Session, args.ChannelId, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES) { - return &model.CommandResponse{Text: args.T("api.command_channel_purpose.permission.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} - } - - if channel.Type == model.CHANNEL_PRIVATE && !a.SessionHasPermissionToChannel(args.Session, args.ChannelId, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES) { - return &model.CommandResponse{Text: args.T("api.command_channel_purpose.permission.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_channel_purpose.channel.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } - if channel.Type == model.CHANNEL_GROUP || channel.Type == model.CHANNEL_DIRECT { - return &model.CommandResponse{Text: args.T("api.command_channel_purpose.direct_group.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + switch channel.Type { + case model.CHANNEL_OPEN: + if !a.SessionHasPermissionToChannel(args.Session, args.ChannelId, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES) { + return &model.CommandResponse{ + Text: args.T("api.command_channel_purpose.permission.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } + } + case model.CHANNEL_PRIVATE: + if !a.SessionHasPermissionToChannel(args.Session, args.ChannelId, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES) { + return &model.CommandResponse{ + Text: args.T("api.command_channel_purpose.permission.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } + } + default: + return &model.CommandResponse{ + Text: args.T("api.command_channel_purpose.direct_group.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } if len(message) == 0 { - return &model.CommandResponse{Text: args.T("api.command_channel_purpose.message.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_channel_purpose.message.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } patch := &model.ChannelPatch{ @@ -62,7 +79,10 @@ func (me *PurposeProvider) DoCommand(a *App, args *model.CommandArgs, message st _, err = a.PatchChannel(channel, patch, args.UserId) if err != nil { - return &model.CommandResponse{Text: args.T("api.command_channel_purpose.update_channel.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_channel_purpose.update_channel.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } return &model.CommandResponse{} diff --git a/app/command_channel_purpose_test.go b/app/command_channel_purpose_test.go new file mode 100644 index 000000000..3bdaa4e4f --- /dev/null +++ b/app/command_channel_purpose_test.go @@ -0,0 +1,93 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mattermost/mattermost-server/model" +) + +func TestPurposeProviderDoCommand(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + pp := PurposeProvider{} + + // Try a public channel *with* permission. + args := &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: th.BasicChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: model.TEAM_USER_ROLE_ID}}}, + } + + for msg, expected := range map[string]string{ + "": "api.command_channel_purpose.message.app_error", + "hello": "", + } { + actual := pp.DoCommand(th.App, args, msg).Text + assert.Equal(t, expected, actual) + } + + // Try a public channel *without* permission. + args = &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: th.BasicChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: ""}}}, + } + + actual := pp.DoCommand(th.App, args, "hello").Text + assert.Equal(t, "api.command_channel_purpose.permission.app_error", actual) + + // Try a private channel *with* permission. + privateChannel := th.CreatePrivateChannel(th.BasicTeam) + + args = &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: privateChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: model.TEAM_USER_ROLE_ID}}}, + } + + actual = pp.DoCommand(th.App, args, "hello").Text + assert.Equal(t, "", actual) + + // Try a private channel *without* permission. + args = &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: privateChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: ""}}}, + } + + actual = pp.DoCommand(th.App, args, "hello").Text + assert.Equal(t, "api.command_channel_purpose.permission.app_error", actual) + + // Try a group channel *with* being a member. + user1 := th.CreateUser() + user2 := th.CreateUser() + + groupChannel := th.CreateGroupChannel(user1, user2) + + args = &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: groupChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: ""}}}, + } + + actual = pp.DoCommand(th.App, args, "hello").Text + assert.Equal(t, "api.command_channel_purpose.direct_group.app_error", actual) + + // Try a direct channel *with* being a member. + directChannel := th.CreateDmChannel(user1) + + args = &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: directChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: ""}}}, + } + + actual = pp.DoCommand(th.App, args, "hello").Text + assert.Equal(t, "api.command_channel_purpose.direct_group.app_error", actual) +} diff --git a/app/command_channel_rename.go b/app/command_channel_rename.go index ddcfea67a..a2e45ed46 100644 --- a/app/command_channel_rename.go +++ b/app/command_channel_rename.go @@ -4,8 +4,9 @@ package app import ( - "github.com/mattermost/mattermost-server/model" goi18n "github.com/nicksnyder/go-i18n/i18n" + + "github.com/mattermost/mattermost-server/model" ) type RenameProvider struct { @@ -36,27 +37,50 @@ func (me *RenameProvider) GetCommand(a *App, T goi18n.TranslateFunc) *model.Comm func (me *RenameProvider) DoCommand(a *App, args *model.CommandArgs, message string) *model.CommandResponse { channel, err := a.GetChannel(args.ChannelId) if err != nil { - return &model.CommandResponse{Text: args.T("api.command_channel_rename.channel.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} - } - - if channel.Type == model.CHANNEL_OPEN && !a.SessionHasPermissionToChannel(args.Session, args.ChannelId, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES) { - return &model.CommandResponse{Text: args.T("api.command_channel_rename.permission.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} - } - - if channel.Type == model.CHANNEL_PRIVATE && !a.SessionHasPermissionToChannel(args.Session, args.ChannelId, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES) { - return &model.CommandResponse{Text: args.T("api.command_channel_rename.permission.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_channel_rename.channel.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } - if channel.Type == model.CHANNEL_GROUP || channel.Type == model.CHANNEL_DIRECT { + switch channel.Type { + case model.CHANNEL_OPEN: + if !a.SessionHasPermissionToChannel(args.Session, args.ChannelId, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES) { + return &model.CommandResponse{ + Text: args.T("api.command_channel_rename.permission.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } + } + case model.CHANNEL_PRIVATE: + if !a.SessionHasPermissionToChannel(args.Session, args.ChannelId, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES) { + return &model.CommandResponse{ + Text: args.T("api.command_channel_rename.permission.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } + } + default: return &model.CommandResponse{Text: args.T("api.command_channel_rename.direct_group.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } if len(message) == 0 { - return &model.CommandResponse{Text: args.T("api.command_channel_rename.message.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_channel_rename.message.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } else if len(message) > model.CHANNEL_NAME_UI_MAX_LENGTH { - return &model.CommandResponse{Text: args.T("api.command_channel_rename.too_long.app_error", map[string]interface{}{"Length": model.CHANNEL_NAME_UI_MAX_LENGTH}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_channel_rename.too_long.app_error", map[string]interface{}{ + "Length": model.CHANNEL_NAME_UI_MAX_LENGTH, + }), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } else if len(message) < model.CHANNEL_NAME_MIN_LENGTH { - return &model.CommandResponse{Text: args.T("api.command_channel_rename.too_short.app_error", map[string]interface{}{"Length": model.CHANNEL_NAME_MIN_LENGTH}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_channel_rename.too_short.app_error", map[string]interface{}{ + "Length": model.CHANNEL_NAME_MIN_LENGTH, + }), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } patch := &model.ChannelPatch{ @@ -66,7 +90,10 @@ func (me *RenameProvider) DoCommand(a *App, args *model.CommandArgs, message str _, err = a.PatchChannel(channel, patch, args.UserId) if err != nil { - return &model.CommandResponse{Text: args.T("api.command_channel_rename.update_channel.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_channel_rename.update_channel.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } return &model.CommandResponse{} diff --git a/app/command_channel_rename_test.go b/app/command_channel_rename_test.go index 9c86b18e0..d4cdeda51 100644 --- a/app/command_channel_rename_test.go +++ b/app/command_channel_rename_test.go @@ -1,10 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + package app import ( "testing" - "github.com/mattermost/mattermost-server/model" "github.com/stretchr/testify/assert" + + "github.com/mattermost/mattermost-server/model" ) func TestRenameProviderDoCommand(t *testing.T) { @@ -29,4 +33,63 @@ func TestRenameProviderDoCommand(t *testing.T) { actual := rp.DoCommand(th.App, args, msg).Text assert.Equal(t, expected, actual) } + + // Try a public channel *without* permission. + args = &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: th.BasicChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: ""}}}, + } + + actual := rp.DoCommand(th.App, args, "hello").Text + assert.Equal(t, "api.command_channel_rename.permission.app_error", actual) + + // Try a private channel *with* permission. + privateChannel := th.CreatePrivateChannel(th.BasicTeam) + + args = &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: privateChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: model.TEAM_USER_ROLE_ID}}}, + } + + actual = rp.DoCommand(th.App, args, "hello").Text + assert.Equal(t, "", actual) + + // Try a private channel *without* permission. + args = &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: privateChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: ""}}}, + } + + actual = rp.DoCommand(th.App, args, "hello").Text + assert.Equal(t, "api.command_channel_rename.permission.app_error", actual) + + // Try a group channel *with* being a member. + user1 := th.CreateUser() + user2 := th.CreateUser() + + groupChannel := th.CreateGroupChannel(user1, user2) + + args = &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: groupChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: ""}}}, + } + + actual = rp.DoCommand(th.App, args, "hello").Text + assert.Equal(t, "api.command_channel_rename.direct_group.app_error", actual) + + // Try a direct channel *with* being a member. + directChannel := th.CreateDmChannel(user1) + + args = &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: directChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: ""}}}, + } + + actual = rp.DoCommand(th.App, args, "hello").Text + assert.Equal(t, "api.command_channel_rename.direct_group.app_error", actual) } diff --git a/app/command_invite.go b/app/command_invite.go index 86cc5fdbb..e6167dad6 100644 --- a/app/command_invite.go +++ b/app/command_invite.go @@ -6,9 +6,10 @@ package app import ( "strings" + goi18n "github.com/nicksnyder/go-i18n/i18n" + "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" - goi18n "github.com/nicksnyder/go-i18n/i18n" ) type InviteProvider struct { @@ -38,7 +39,10 @@ func (me *InviteProvider) GetCommand(a *App, T goi18n.TranslateFunc) *model.Comm func (me *InviteProvider) DoCommand(a *App, args *model.CommandArgs, message string) *model.CommandResponse { if message == "" { - return &model.CommandResponse{Text: args.T("api.command_invite.missing_message.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_invite.missing_message.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } splitMessage := strings.SplitN(message, " ", 2) @@ -48,7 +52,10 @@ func (me *InviteProvider) DoCommand(a *App, args *model.CommandArgs, message str var userProfile *model.User if result := <-a.Srv.Store.User().GetByUsername(targetUsername); result.Err != nil { mlog.Error(result.Err.Error()) - return &model.CommandResponse{Text: args.T("api.command_invite.missing_user.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_invite.missing_user.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } else { userProfile = result.Data.(*model.User) } @@ -60,49 +67,89 @@ func (me *InviteProvider) DoCommand(a *App, args *model.CommandArgs, message str targetChannelName := strings.TrimPrefix(strings.TrimSpace(splitMessage[1]), "~") if channelToJoin, err = a.GetChannelByName(targetChannelName, args.TeamId, false); err != nil { - return &model.CommandResponse{Text: args.T("api.command_invite.channel.error", map[string]interface{}{"Channel": targetChannelName}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_invite.channel.error", map[string]interface{}{ + "Channel": targetChannelName, + }), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } } else { channelToJoin, err = a.GetChannel(args.ChannelId) if err != nil { - return &model.CommandResponse{Text: args.T("api.command_invite.channel.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_invite.channel.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } } - // Check if is a Direct Channel - if channelToJoin.Type == model.CHANNEL_DIRECT || channelToJoin.Type == model.CHANNEL_GROUP { - return &model.CommandResponse{Text: args.T("api.command_invite.directchannel.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} - } - - // Check Permissions - if channelToJoin.Type == model.CHANNEL_OPEN && !a.SessionHasPermissionToChannel(args.Session, channelToJoin.Id, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS) { - return &model.CommandResponse{Text: args.T("api.command_invite.permission.app_error", map[string]interface{}{"User": userProfile.Username, "Channel": channelToJoin.Name}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} - } - - // Check if the user who wants to add another is trying to add in a pvt channel, but does not have permission - // but is in the channel - _, err = a.GetChannelMember(channelToJoin.Id, args.UserId) - if channelToJoin.Type == model.CHANNEL_PRIVATE && !a.SessionHasPermissionToChannel(args.Session, channelToJoin.Id, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS) && err == nil { - return &model.CommandResponse{Text: args.T("api.command_invite.permission.app_error", map[string]interface{}{"User": userProfile.Username, "Channel": channelToJoin.Name}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} - } - - // In this case just check if is a pvt channel and user has permission - if channelToJoin.Type == model.CHANNEL_PRIVATE && !a.SessionHasPermissionToChannel(args.Session, channelToJoin.Id, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS) { - return &model.CommandResponse{Text: args.T("api.command_invite.private_channel.app_error", map[string]interface{}{"Channel": channelToJoin.Name}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + // Permissions Check + switch channelToJoin.Type { + case model.CHANNEL_OPEN: + if !a.SessionHasPermissionToChannel(args.Session, channelToJoin.Id, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS) { + return &model.CommandResponse{ + Text: args.T("api.command_invite.permission.app_error", map[string]interface{}{ + "User": userProfile.Username, + "Channel": channelToJoin.Name, + }), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } + } + case model.CHANNEL_PRIVATE: + if !a.SessionHasPermissionToChannel(args.Session, channelToJoin.Id, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS) { + if _, err = a.GetChannelMember(channelToJoin.Id, args.UserId); err == nil { + // User doing the inviting is a member of the channel. + return &model.CommandResponse{ + Text: args.T("api.command_invite.permission.app_error", map[string]interface{}{ + "User": userProfile.Username, + "Channel": channelToJoin.Name, + }), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } + } else { + // User doing the inviting is *not* a member of the channel. + return &model.CommandResponse{ + Text: args.T("api.command_invite.private_channel.app_error", map[string]interface{}{ + "Channel": channelToJoin.Name, + }), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } + } + } + default: + return &model.CommandResponse{ + Text: args.T("api.command_invite.directchannel.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } // Check if user is already in the channel _, err = a.GetChannelMember(channelToJoin.Id, userProfile.Id) if err == nil { - return &model.CommandResponse{Text: args.T("api.command_invite.user_already_in_channel.app_error", map[string]interface{}{"User": userProfile.Username}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_invite.user_already_in_channel.app_error", map[string]interface{}{ + "User": userProfile.Username, + }), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } if _, err := a.AddChannelMember(userProfile.Id, channelToJoin, args.Session.UserId, ""); err != nil { - return &model.CommandResponse{Text: args.T("api.command_invite.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_invite.fail.app_error"), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } if args.ChannelId != channelToJoin.Id { - return &model.CommandResponse{Text: args.T("api.command_invite.success", map[string]interface{}{"User": userProfile.Username, "Channel": channelToJoin.Name}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{ + Text: args.T("api.command_invite.success", map[string]interface{}{ + "User": userProfile.Username, + "Channel": channelToJoin.Name, + }), + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + } } return &model.CommandResponse{} diff --git a/app/command_invite_test.go b/app/command_invite_test.go index 0d1db4a07..f141cb239 100644 --- a/app/command_invite_test.go +++ b/app/command_invite_test.go @@ -95,12 +95,7 @@ func TestInviteProvider(t *testing.T) { msg: basicUser4.Username, }, { - desc: "try to add a user to a direct channel", - expected: "api.command_invite.directchannel.app_error", - msg: userAndDMChannel, - }, - { - desc: "try to add a user to a privante channel with no permission", + desc: "try to add a user to a private channel with no permission", expected: "api.command_invite.private_channel.app_error", msg: userAndInvalidPrivate, }, diff --git a/app/command_join.go b/app/command_join.go index 61ed65ba6..b913014b8 100644 --- a/app/command_join.go +++ b/app/command_join.go @@ -4,9 +4,11 @@ package app import ( - "github.com/mattermost/mattermost-server/model" - goi18n "github.com/nicksnyder/go-i18n/i18n" "strings" + + goi18n "github.com/nicksnyder/go-i18n/i18n" + + "github.com/mattermost/mattermost-server/model" ) type JoinProvider struct { @@ -41,33 +43,38 @@ func (me *JoinProvider) DoCommand(a *App, args *model.CommandArgs, message strin channelName = message[1:] } - if result := <-a.Srv.Store.Channel().GetByName(args.TeamId, channelName, true); result.Err != nil { + result := <-a.Srv.Store.Channel().GetByName(args.TeamId, channelName, true) + if result.Err != nil { return &model.CommandResponse{Text: args.T("api.command_join.list.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} - } else { - channel := result.Data.(*model.Channel) + } - if channel.Name == channelName { - allowed := false - if (channel.Type == model.CHANNEL_PRIVATE && a.SessionHasPermissionToChannel(args.Session, channel.Id, model.PERMISSION_READ_CHANNEL)) || channel.Type == model.CHANNEL_OPEN { - allowed = true - } + channel := result.Data.(*model.Channel) - if !allowed { - return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} - } + if channel.Name != channelName { + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: args.T("api.command_join.missing.app_error")} + } - if err := a.JoinChannel(channel, args.UserId); err != nil { - return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} - } + switch channel.Type { + case model.CHANNEL_OPEN: + if !a.SessionHasPermissionToChannel(args.Session, channel.Id, model.PERMISSION_JOIN_PUBLIC_CHANNELS) { + return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + case model.CHANNEL_PRIVATE: + if !a.SessionHasPermissionToChannel(args.Session, channel.Id, model.PERMISSION_READ_CHANNEL) { + return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + default: + return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } - team, err := a.GetTeam(channel.TeamId) - if err != nil { - return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} - } + if err := a.JoinChannel(channel, args.UserId); err != nil { + return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } - return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + channel.Name} - } + team, err := a.GetTeam(channel.TeamId) + if err != nil { + return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } - return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: args.T("api.command_join.missing.app_error")} + return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + channel.Name} } diff --git a/app/command_join_test.go b/app/command_join_test.go index 77574217b..e5f42f31e 100644 --- a/app/command_join_test.go +++ b/app/command_join_test.go @@ -5,9 +5,11 @@ package app import ( "testing" - "github.com/mattermost/mattermost-server/model" + "github.com/nicksnyder/go-i18n/i18n" "github.com/stretchr/testify/assert" + + "github.com/mattermost/mattermost-server/model" ) func TestJoinCommandNoChannel(t *testing.T) { @@ -20,10 +22,11 @@ func TestJoinCommandNoChannel(t *testing.T) { cmd := &JoinProvider{} resp := cmd.DoCommand(th.App, &model.CommandArgs{ - T: i18n.IdentityTfunc(), - UserId: th.BasicUser2.Id, + T: i18n.IdentityTfunc(), + UserId: th.BasicUser2.Id, SiteURL: "http://test.url", - TeamId: th.BasicTeam.Id, + TeamId: th.BasicTeam.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: model.TEAM_USER_ROLE_ID}}}, }, "asdsad") assert.Equal(t, "api.command_join.list.app_error", resp.Text) @@ -38,20 +41,20 @@ func TestJoinCommandForExistingChannel(t *testing.T) { } channel2, _ := th.App.CreateChannel(&model.Channel{ - DisplayName: "AA", - Name: "aa" + model.NewId() + "a", - Type: model.CHANNEL_OPEN, - TeamId: th.BasicTeam.Id, - CreatorId: th.BasicUser.Id, + DisplayName: "AA", + Name: "aa" + model.NewId() + "a", + Type: model.CHANNEL_OPEN, + TeamId: th.BasicTeam.Id, + CreatorId: th.BasicUser.Id, }, false) - cmd := &JoinProvider{} resp := cmd.DoCommand(th.App, &model.CommandArgs{ - T: i18n.IdentityTfunc(), - UserId: th.BasicUser2.Id, + T: i18n.IdentityTfunc(), + UserId: th.BasicUser2.Id, SiteURL: "http://test.url", - TeamId: th.BasicTeam.Id, + TeamId: th.BasicTeam.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: model.TEAM_USER_ROLE_ID}}}, }, channel2.Name) assert.Equal(t, "", resp.Text) @@ -67,22 +70,81 @@ func TestJoinCommandWithTilde(t *testing.T) { } channel2, _ := th.App.CreateChannel(&model.Channel{ - DisplayName: "AA", - Name: "aa" + model.NewId() + "a", - Type: model.CHANNEL_OPEN, - TeamId: th.BasicTeam.Id, - CreatorId: th.BasicUser.Id, + DisplayName: "AA", + Name: "aa" + model.NewId() + "a", + Type: model.CHANNEL_OPEN, + TeamId: th.BasicTeam.Id, + CreatorId: th.BasicUser.Id, }, false) - cmd := &JoinProvider{} resp := cmd.DoCommand(th.App, &model.CommandArgs{ - T: i18n.IdentityTfunc(), - UserId: th.BasicUser2.Id, + T: i18n.IdentityTfunc(), + UserId: th.BasicUser2.Id, SiteURL: "http://test.url", - TeamId: th.BasicTeam.Id, + TeamId: th.BasicTeam.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: model.TEAM_USER_ROLE_ID}}}, }, "~"+channel2.Name) assert.Equal(t, "", resp.Text) assert.Equal(t, "http://test.url/"+th.BasicTeam.Name+"/channels/"+channel2.Name, resp.GotoLocation) } + +func TestJoinCommandPermissions(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + channel2, _ := th.App.CreateChannel(&model.Channel{ + DisplayName: "AA", + Name: "aa" + model.NewId() + "a", + Type: model.CHANNEL_OPEN, + TeamId: th.BasicTeam.Id, + CreatorId: th.BasicUser.Id, + }, false) + + cmd := &JoinProvider{} + + // Try a public channel *without* permission. + args := &model.CommandArgs{ + T: i18n.IdentityTfunc(), + UserId: th.BasicUser2.Id, + SiteURL: "http://test.url", + TeamId: th.BasicTeam.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: ""}}}, + } + + actual := cmd.DoCommand(th.App, args, "~"+channel2.Name).Text + assert.Equal(t, "api.command_join.fail.app_error", actual) + + // Try a public channel with permission. + args = &model.CommandArgs{ + T: i18n.IdentityTfunc(), + UserId: th.BasicUser2.Id, + SiteURL: "http://test.url", + TeamId: th.BasicTeam.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: model.TEAM_USER_ROLE_ID}}}, + } + + actual = cmd.DoCommand(th.App, args, "~"+channel2.Name).Text + assert.Equal(t, "", actual) + + // Try a private channel *without* permission. + channel3, _ := th.App.CreateChannel(&model.Channel{ + DisplayName: "BB", + Name: "aa" + model.NewId() + "a", + Type: model.CHANNEL_PRIVATE, + TeamId: th.BasicTeam.Id, + CreatorId: th.BasicUser.Id, + }, false) + + args = &model.CommandArgs{ + T: i18n.IdentityTfunc(), + UserId: th.BasicUser2.Id, + SiteURL: "http://test.url", + TeamId: th.BasicTeam.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: model.TEAM_USER_ROLE_ID}}}, + } + + actual = cmd.DoCommand(th.App, args, "~"+channel3.Name).Text + assert.Equal(t, "api.command_join.fail.app_error", actual) +} diff --git a/app/command_remove.go b/app/command_remove.go index 3671a2063..6a67996e9 100644 --- a/app/command_remove.go +++ b/app/command_remove.go @@ -70,15 +70,16 @@ func doCommand(a *App, args *model.CommandArgs, message string) *model.CommandRe return &model.CommandResponse{Text: args.T("api.command_channel_rename.channel.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } - if channel.Type == model.CHANNEL_OPEN && !a.SessionHasPermissionToChannel(args.Session, args.ChannelId, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS) { - return &model.CommandResponse{Text: args.T("api.command_remove.permission.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} - } - - if channel.Type == model.CHANNEL_PRIVATE && !a.SessionHasPermissionToChannel(args.Session, args.ChannelId, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS) { - return &model.CommandResponse{Text: args.T("api.command_remove.permission.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} - } - - if channel.Type == model.CHANNEL_GROUP || channel.Type == model.CHANNEL_DIRECT { + switch channel.Type { + case model.CHANNEL_OPEN: + if !a.SessionHasPermissionToChannel(args.Session, args.ChannelId, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS) { + return &model.CommandResponse{Text: args.T("api.command_remove.permission.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + case model.CHANNEL_PRIVATE: + if !a.SessionHasPermissionToChannel(args.Session, args.ChannelId, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS) { + return &model.CommandResponse{Text: args.T("api.command_remove.permission.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + default: return &model.CommandResponse{Text: args.T("api.command_remove.direct_group.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } diff --git a/app/command_remove_test.go b/app/command_remove_test.go new file mode 100644 index 000000000..f17a70bad --- /dev/null +++ b/app/command_remove_test.go @@ -0,0 +1,109 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mattermost/mattermost-server/model" +) + +func TestRemoveProviderDoCommand(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + rp := RemoveProvider{} + + publicChannel, _ := th.App.CreateChannel(&model.Channel{ + DisplayName: "AA", + Name: "aa" + model.NewId() + "a", + Type: model.CHANNEL_OPEN, + TeamId: th.BasicTeam.Id, + CreatorId: th.BasicUser.Id, + }, false) + + privateChannel, _ := th.App.CreateChannel(&model.Channel{ + DisplayName: "BB", + Name: "aa" + model.NewId() + "a", + Type: model.CHANNEL_OPEN, + TeamId: th.BasicTeam.Id, + CreatorId: th.BasicUser.Id, + }, false) + + targetUser := th.CreateUser() + th.App.AddUserToTeam(th.BasicTeam.Id, targetUser.Id, targetUser.Id) + th.App.AddUserToChannel(targetUser, publicChannel) + th.App.AddUserToChannel(targetUser, privateChannel) + + // Try a public channel *without* permission. + args := &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: publicChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: ""}}}, + } + + actual := rp.DoCommand(th.App, args, targetUser.Username).Text + assert.Equal(t, "api.command_remove.permission.app_error", actual) + + // Try a public channel *with* permission. + th.App.AddUserToChannel(th.BasicUser, publicChannel) + args = &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: publicChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: ""}}}, + } + + actual = rp.DoCommand(th.App, args, targetUser.Username).Text + assert.Equal(t, "", actual) + + // Try a private channel *without* permission. + args = &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: privateChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: ""}}}, + } + + actual = rp.DoCommand(th.App, args, targetUser.Username).Text + assert.Equal(t, "api.command_remove.permission.app_error", actual) + + // Try a private channel *with* permission. + th.App.AddUserToChannel(th.BasicUser, privateChannel) + args = &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: privateChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: ""}}}, + } + + actual = rp.DoCommand(th.App, args, targetUser.Username).Text + assert.Equal(t, "", actual) + + // Try a group channel + user1 := th.CreateUser() + user2 := th.CreateUser() + + groupChannel := th.CreateGroupChannel(user1, user2) + + args = &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: groupChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: ""}}}, + } + + actual = rp.DoCommand(th.App, args, user1.Username).Text + assert.Equal(t, "api.command_remove.direct_group.app_error", actual) + + // Try a direct channel *with* being a member. + directChannel := th.CreateDmChannel(user1) + + args = &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: directChannel.Id, + Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: ""}}}, + } + + actual = rp.DoCommand(th.App, args, user1.Username).Text + assert.Equal(t, "api.command_remove.direct_group.app_error", actual) +} diff --git a/app/config_test.go b/app/config_test.go index eb3fa8a53..abaf00167 100644 --- a/app/config_test.go +++ b/app/config_test.go @@ -122,12 +122,10 @@ func TestEnsureInstallationDate(t *testing.T) { sqlStore := th.App.Srv.Store.User().(*sqlstore.SqlUserStore) sqlStore.GetMaster().Exec("DELETE FROM Users") - var users []*model.User for _, createAt := range tc.UsersCreationDates { user := th.CreateUser() user.CreateAt = createAt sqlStore.GetMaster().Exec("UPDATE Users SET CreateAt = :CreateAt WHERE Id = :UserId", map[string]interface{}{"CreateAt": createAt, "UserId": user.Id}) - users = append(users, user) } if tc.PrevInstallationDate == nil { diff --git a/app/diagnostics_test.go b/app/diagnostics_test.go index 1dfcbecd1..8d4e57107 100644 --- a/app/diagnostics_test.go +++ b/app/diagnostics_test.go @@ -103,19 +103,13 @@ func TestDiagnostics(t *testing.T) { info := "" // Collect the info sent. + Loop: for { - done := false select { case result := <-data: info += result case <-time.After(time.Second * 1): - // Done recieving - done = true - break - } - - if done { - break + break Loop } } diff --git a/app/file.go b/app/file.go index d2a145c81..278990b49 100644 --- a/app/file.go +++ b/app/file.go @@ -613,19 +613,18 @@ func (a *App) GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) { } func (a *App) CopyFileInfos(userId string, fileIds []string) ([]string, *model.AppError) { - newFileIds := []string{} + var newFileIds []string now := model.GetMillis() for _, fileId := range fileIds { - fileInfo := &model.FileInfo{} + result := <-a.Srv.Store.FileInfo().Get(fileId) - if result := <-a.Srv.Store.FileInfo().Get(fileId); result.Err != nil { + if result.Err != nil { return nil, result.Err - } else { - fileInfo = result.Data.(*model.FileInfo) } + fileInfo := result.Data.(*model.FileInfo) fileInfo.Id = model.NewId() fileInfo.CreatorId = userId fileInfo.CreateAt = now diff --git a/app/http_service.go b/app/http_service.go new file mode 100644 index 000000000..71e72ab2f --- /dev/null +++ b/app/http_service.go @@ -0,0 +1,67 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "net" + "net/http" + "strings" + + "github.com/mattermost/mattermost-server/utils" +) + +// Wraps the functionality for creating a new http.Client to encapsulate that and allow it to be mocked when testing +type HTTPService interface { + MakeClient(trustURLs bool) *http.Client + Close() +} + +type HTTPServiceImpl struct { + app *App +} + +func MakeHTTPService(app *App) HTTPService { + return &HTTPServiceImpl{app} +} + +func (h *HTTPServiceImpl) MakeClient(trustURLs bool) *http.Client { + insecure := h.app.Config().ServiceSettings.EnableInsecureOutgoingConnections != nil && *h.app.Config().ServiceSettings.EnableInsecureOutgoingConnections + + if trustURLs { + return utils.NewHTTPClient(insecure, nil, nil) + } + + allowHost := func(host string) bool { + if h.app.Config().ServiceSettings.AllowedUntrustedInternalConnections == nil { + return false + } + for _, allowed := range strings.Fields(*h.app.Config().ServiceSettings.AllowedUntrustedInternalConnections) { + if host == allowed { + return true + } + } + return false + } + + allowIP := func(ip net.IP) bool { + if !utils.IsReservedIP(ip) { + return true + } + if h.app.Config().ServiceSettings.AllowedUntrustedInternalConnections == nil { + return false + } + for _, allowed := range strings.Fields(*h.app.Config().ServiceSettings.AllowedUntrustedInternalConnections) { + if _, ipRange, err := net.ParseCIDR(allowed); err == nil && ipRange.Contains(ip) { + return true + } + } + return false + } + + return utils.NewHTTPClient(insecure, allowHost, allowIP) +} + +func (h *HTTPServiceImpl) Close() { + // Does nothing, but allows this to be overridden when mocking the service +} diff --git a/app/http_service_test.go b/app/http_service_test.go new file mode 100644 index 000000000..396a991b1 --- /dev/null +++ b/app/http_service_test.go @@ -0,0 +1,65 @@ +package app + +import ( + "io/ioutil" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMockHTTPService(t *testing.T) { + getCalled := false + putCalled := false + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/get" && r.Method == http.MethodGet { + getCalled = true + + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + } else if r.URL.Path == "/put" && r.Method == http.MethodPut { + putCalled = true + + w.WriteHeader(http.StatusCreated) + w.Write([]byte("CREATED")) + } else { + w.WriteHeader(http.StatusNotFound) + } + }) + + th := Setup().MockHTTPService(handler) + defer th.TearDown() + + url := th.MockedHTTPService.Server.URL + + t.Run("GET", func(t *testing.T) { + client := th.App.HTTPService.MakeClient(false) + + resp, err := client.Get(url + "/get") + defer consumeAndClose(resp) + + bodyContents, _ := ioutil.ReadAll(resp.Body) + + require.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "OK", string(bodyContents)) + assert.True(t, getCalled) + }) + + t.Run("PUT", func(t *testing.T) { + client := th.App.HTTPService.MakeClient(false) + + request, _ := http.NewRequest(http.MethodPut, url+"/put", nil) + resp, err := client.Do(request) + defer consumeAndClose(resp) + + bodyContents, _ := ioutil.ReadAll(resp.Body) + + require.Nil(t, err) + assert.Equal(t, http.StatusCreated, resp.StatusCode) + assert.Equal(t, "CREATED", string(bodyContents)) + assert.True(t, putCalled) + }) +} diff --git a/app/import_functions.go b/app/import_functions.go index 1490dc6fa..dce84feb5 100644 --- a/app/import_functions.go +++ b/app/import_functions.go @@ -123,9 +123,9 @@ func (a *App) ImportRole(data *RoleImportData, dryRun bool, isSchemeRole bool) * } if len(role.Id) == 0 { - role, err = a.CreateRole(role) + _, err = a.CreateRole(role) } else { - role, err = a.UpdateRole(role) + _, err = a.UpdateRole(role) } return err diff --git a/app/notification_email.go b/app/notification_email.go index cccd02eba..f5b55f9a8 100644 --- a/app/notification_email.go +++ b/app/notification_email.go @@ -306,7 +306,7 @@ func getFormattedPostTime(user *model.User, post *model.Post, useMilitaryTime bo Year: fmt.Sprintf("%d", localTime.Year()), Month: translateFunc(localTime.Month().String()), Day: fmt.Sprintf("%d", localTime.Day()), - Hour: fmt.Sprintf("%s", hour), + Hour: hour, Minute: fmt.Sprintf("%02d"+period, localTime.Minute()), TimeZone: zone, } diff --git a/app/notification_push.go b/app/notification_push.go index 12d9f5258..517988a97 100644 --- a/app/notification_push.go +++ b/app/notification_push.go @@ -184,7 +184,7 @@ func (a *App) sendToPushProxy(msg model.PushNotification, session *model.Session request, _ := http.NewRequest("POST", strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/")+model.API_URL_SUFFIX_V1+"/send_push", strings.NewReader(msg.ToJson())) - if resp, err := a.HTTPClient(true).Do(request); err != nil { + if resp, err := a.HTTPService.MakeClient(true).Do(request); err != nil { mlog.Error(fmt.Sprintf("Device push reported as error for UserId=%v SessionId=%v message=%v", session.UserId, session.Id, err.Error()), mlog.String("user_id", session.UserId)) } else { pushResponse := model.PushResponseFromJson(resp.Body) diff --git a/app/oauth.go b/app/oauth.go index a0123c0e9..645d502f5 100644 --- a/app/oauth.go +++ b/app/oauth.go @@ -761,7 +761,7 @@ func (a *App) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service var ar *model.AccessResponse var bodyBytes []byte - if resp, err := a.HTTPClient(true).Do(req); err != nil { + if resp, err := a.HTTPService.MakeClient(true).Do(req); err != nil { return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, err.Error(), http.StatusInternalServerError) } else { bodyBytes, _ = ioutil.ReadAll(resp.Body) @@ -791,7 +791,7 @@ func (a *App) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "Bearer "+ar.AccessToken) - if resp, err := a.HTTPClient(true).Do(req); err != nil { + if resp, err := a.HTTPService.MakeClient(true).Do(req); err != nil { return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", map[string]interface{}{"Service": service}, err.Error(), http.StatusInternalServerError) } else { bodyBytes, _ = ioutil.ReadAll(resp.Body) diff --git a/app/permissions.go b/app/permissions.go index 8b4df7c56..ca5c8a165 100644 --- a/app/permissions.go +++ b/app/permissions.go @@ -138,11 +138,7 @@ func (a *App) ExportPermissions(w io.Writer) error { schemeExport = append(schemeExport, []byte("\n")...) _, err = w.Write(schemeExport) - if err != nil { - return err - } - - return nil + return err } func (a *App) ImportPermissions(jsonl io.Reader) error { diff --git a/app/plugin_hooks_test.go b/app/plugin_hooks_test.go index f098374ad..f2acd73e4 100644 --- a/app/plugin_hooks_test.go +++ b/app/plugin_hooks_test.go @@ -13,6 +13,8 @@ import ( "testing" "time" + "github.com/pkg/errors" + "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/plugin" "github.com/mattermost/mattermost-server/plugin/plugintest" @@ -33,7 +35,7 @@ func compileGo(t *testing.T, sourceCode, outputPath string) { require.NoError(t, cmd.Run()) } -func SetAppEnvironmentWithPlugins(t *testing.T, pluginCode []string, app *App, apiFunc func(*model.Manifest) plugin.API) { +func SetAppEnvironmentWithPlugins(t *testing.T, pluginCode []string, app *App, apiFunc func(*model.Manifest) plugin.API) []error { pluginDir, err := ioutil.TempDir("", "") require.NoError(t, err) webappPluginDir, err := ioutil.TempDir("", "") @@ -45,14 +47,18 @@ func SetAppEnvironmentWithPlugins(t *testing.T, pluginCode []string, app *App, a require.NoError(t, err) app.Plugins = env + activationErrors := []error{} for _, code := range pluginCode { pluginId := model.NewId() backend := filepath.Join(pluginDir, pluginId, "backend.exe") compileGo(t, code, backend) ioutil.WriteFile(filepath.Join(pluginDir, pluginId, "plugin.json"), []byte(`{"id": "`+pluginId+`", "backend": {"executable": "backend.exe"}}`), 0600) - env.Activate(pluginId) + _, _, activationErr := env.Activate(pluginId) + activationErrors = append(activationErrors, activationErr) } + + return activationErrors } func TestHookMessageWillBePosted(t *testing.T) { @@ -89,7 +95,7 @@ func TestHookMessageWillBePosted(t *testing.T) { Message: "message_", CreateAt: model.GetMillis() - 10000, } - post, err := th.App.CreatePost(post, th.BasicChannel, false) + _, err := th.App.CreatePost(post, th.BasicChannel, false) if assert.NotNil(t, err) { assert.Equal(t, "Post rejected by plugin. rejected", err.Message) } @@ -129,7 +135,7 @@ func TestHookMessageWillBePosted(t *testing.T) { Message: "message_", CreateAt: model.GetMillis() - 10000, } - post, err := th.App.CreatePost(post, th.BasicChannel, false) + _, err := th.App.CreatePost(post, th.BasicChannel, false) if assert.NotNil(t, err) { assert.Equal(t, "Post rejected by plugin. rejected", err.Message) } @@ -327,7 +333,7 @@ func TestHookMessageHasBeenPosted(t *testing.T) { Message: "message", CreateAt: model.GetMillis() - 10000, } - post, err := th.App.CreatePost(post, th.BasicChannel, false) + _, err := th.App.CreatePost(post, th.BasicChannel, false) if err != nil { t.Fatal(err) } @@ -424,7 +430,7 @@ func TestHookMessageHasBeenUpdated(t *testing.T) { } assert.Equal(t, "message_", post.Message) post.Message = post.Message + "edited" - post, err = th.App.UpdatePost(post, true) + _, err = th.App.UpdatePost(post, true) if err != nil { t.Fatal(err) } @@ -785,9 +791,79 @@ func TestUserHasLoggedIn(t *testing.T) { time.Sleep(2 * time.Second) - user, err = th.App.GetUser(th.BasicUser.Id) + user, _ = th.App.GetUser(th.BasicUser.Id) if user.FirstName != "plugin-callback-success" { t.Errorf("Expected firstname overwrite, got default") } } + +func TestErrorString(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + t.Run("errors.New", func(t *testing.T) { + activationErrors := SetAppEnvironmentWithPlugins(t, + []string{ + ` + package main + + import ( + "github.com/pkg/errors" + + "github.com/mattermost/mattermost-server/plugin" + ) + + type MyPlugin struct { + plugin.MattermostPlugin + } + + func (p *MyPlugin) OnActivate() error { + return errors.New("simulate failure") + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + `}, th.App, th.App.NewPluginAPI) + + require.Len(t, activationErrors, 1) + require.NotNil(t, activationErrors[0]) + require.Contains(t, activationErrors[0].Error(), "simulate failure") + }) + + t.Run("AppError", func(t *testing.T) { + activationErrors := SetAppEnvironmentWithPlugins(t, + []string{ + ` + package main + + import ( + "github.com/mattermost/mattermost-server/plugin" + "github.com/mattermost/mattermost-server/model" + ) + + type MyPlugin struct { + plugin.MattermostPlugin + } + + func (p *MyPlugin) OnActivate() error { + return model.NewAppError("where", "id", map[string]interface{}{"param": 1}, "details", 42) + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + `}, th.App, th.App.NewPluginAPI) + + require.Len(t, activationErrors, 1) + require.NotNil(t, activationErrors[0]) + + cause := errors.Cause(activationErrors[0]) + require.IsType(t, &model.AppError{}, cause) + + // params not expected, since not exported + expectedErr := model.NewAppError("where", "id", nil, "details", 42) + require.Equal(t, expectedErr, cause) + }) +} diff --git a/app/post.go b/app/post.go index 8cfc6d659..114029f44 100644 --- a/app/post.go +++ b/app/post.go @@ -779,7 +779,7 @@ func (a *App) GetFileInfosForPost(postId string, readFromMaster bool) ([]*model. func (a *App) GetOpenGraphMetadata(requestURL string) *opengraph.OpenGraph { og := opengraph.NewOpenGraph() - res, err := a.HTTPClient(false).Get(requestURL) + res, err := a.HTTPService.MakeClient(false).Get(requestURL) if err != nil { mlog.Error(fmt.Sprintf("GetOpenGraphMetadata request failed for url=%v with err=%v", requestURL, err.Error())) return og @@ -892,9 +892,9 @@ func (a *App) DoPostAction(postId, actionId, userId, selectedOption string) *mod siteURL, _ := url.Parse(*a.Config().ServiceSettings.SiteURL) subpath, _ := utils.GetSubpathFromConfig(a.Config()) if (url.Hostname() == "localhost" || url.Hostname() == "127.0.0.1" || url.Hostname() == siteURL.Hostname()) && strings.HasPrefix(url.Path, path.Join(subpath, "plugins")) { - httpClient = a.HTTPClient(true) + httpClient = a.HTTPService.MakeClient(true) } else { - httpClient = a.HTTPClient(false) + httpClient = a.HTTPService.MakeClient(false) } resp, err := httpClient.Do(req) diff --git a/app/session_test.go b/app/session_test.go index bf8198a4e..8349a4cec 100644 --- a/app/session_test.go +++ b/app/session_test.go @@ -54,8 +54,6 @@ func TestGetSessionIdleTimeoutInMinutes(t *testing.T) { require.Nil(t, err) assert.Equal(t, rsession.Id, session.Id) - rsession, err = th.App.GetSession(session.Token) - // Test regular session, should timeout time := session.LastActivityAt - (1000 * 60 * 6) <-th.App.Srv.Store.Session().UpdateLastActivityAt(session.Id, time) diff --git a/app/team.go b/app/team.go index dd372a99a..e7b25dddf 100644 --- a/app/team.go +++ b/app/team.go @@ -468,7 +468,7 @@ func (a *App) JoinUserToTeam(team *model.Team, user *model.User, userRequestorId if a.PluginsReady() { var actor *model.User if userRequestorId != "" { - actor, err = a.GetUser(userRequestorId) + actor, _ = a.GetUser(userRequestorId) } a.Go(func() { @@ -798,7 +798,7 @@ func (a *App) LeaveTeam(team *model.Team, user *model.User, requestorId string) if a.PluginsReady() { var actor *model.User if requestorId != "" { - actor, err = a.GetUser(requestorId) + actor, _ = a.GetUser(requestorId) } a.Go(func() { diff --git a/app/user.go b/app/user.go index fa4f36ff1..c8df2ca26 100644 --- a/app/user.go +++ b/app/user.go @@ -1176,11 +1176,7 @@ func (a *App) SendPasswordReset(email string, siteURL string) (bool, *model.AppE return false, err } - if _, err := a.SendPasswordResetEmail(user.Email, token, user.Locale, siteURL); err != nil { - return false, model.NewAppError("SendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message, http.StatusInternalServerError) - } - - return true, nil + return a.SendPasswordResetEmail(user.Email, token, user.Locale, siteURL) } func (a *App) CreatePasswordRecoveryToken(userId string) (*model.Token, *model.AppError) { diff --git a/app/webhook.go b/app/webhook.go index e801b0467..f0264c0c6 100644 --- a/app/webhook.go +++ b/app/webhook.go @@ -107,7 +107,7 @@ func (a *App) TriggerWebhook(payload *model.OutgoingWebhookPayload, hook *model. req, _ := http.NewRequest("POST", url, body) req.Header.Set("Content-Type", contentType) req.Header.Set("Accept", "application/json") - if resp, err := a.HTTPClient(false).Do(req); err != nil { + if resp, err := a.HTTPService.MakeClient(false).Do(req); err != nil { mlog.Error(fmt.Sprintf("Event POST failed, err=%s", err.Error())) } else { defer consumeAndClose(resp) diff --git a/app/webrtc.go b/app/webrtc.go index b10450cab..08601a98c 100644 --- a/app/webrtc.go +++ b/app/webrtc.go @@ -59,7 +59,7 @@ func (a *App) GetWebrtcToken(sessionId string) (string, *model.AppError) { rq, _ := http.NewRequest("POST", *a.Config().WebrtcSettings.GatewayAdminUrl, strings.NewReader(model.MapToJson(data))) rq.Header.Set("Content-Type", "application/json") - if rp, err := a.HTTPClient(true).Do(rq); err != nil { + if rp, err := a.HTTPService.MakeClient(true).Do(rq); err != nil { return "", model.NewAppError("WebRTC.Token", "model.client.connecting.app_error", nil, err.Error(), http.StatusInternalServerError) } else if rp.StatusCode >= 300 { defer consumeAndClose(rp) @@ -93,5 +93,5 @@ func (a *App) RevokeWebrtcToken(sessionId string) { rq.Header.Set("Content-Type", "application/json") // we do not care about the response - a.HTTPClient(true).Do(rq) + a.HTTPService.MakeClient(true).Do(rq) } diff --git a/cmd/mattermost/commands/channel.go b/cmd/mattermost/commands/channel.go index c57a0702d..270ed6586 100644 --- a/cmd/mattermost/commands/channel.go +++ b/cmd/mattermost/commands/channel.go @@ -35,11 +35,12 @@ var ChannelRenameCmd = &cobra.Command{ } var RemoveChannelUsersCmd = &cobra.Command{ - Use: "remove [channel] [users]", - Short: "Remove users from channel", - Long: "Remove some users from channel", - Example: " channel remove myteam:mychannel user@example.com username", - RunE: removeChannelUsersCmdF, + Use: "remove [channel] [users]", + Short: "Remove users from channel", + Long: "Remove some users from channel", + Example: ` channel remove myteam:mychannel user@example.com username + channel remove myteam:mychannel --all-users`, + RunE: removeChannelUsersCmdF, } var AddChannelUsersCmd = &cobra.Command{ @@ -125,6 +126,8 @@ func init() { ChannelRenameCmd.Flags().String("display_name", "", "Channel Display Name") + RemoveChannelUsersCmd.Flags().Bool("all-users", false, "Remove all users from the indicated channel.") + ChannelCmd.AddCommand( ChannelCreateCmd, RemoveChannelUsersCmd, @@ -198,8 +201,14 @@ func removeChannelUsersCmdF(command *cobra.Command, args []string) error { } defer a.Shutdown() - if len(args) < 2 { - return errors.New("Not enough arguments.") + allUsers, _ := command.Flags().GetBool("all-users") + + if allUsers && len(args) != 1 { + return errors.New("individual users must not be specified in conjunction with the --all-users flag") + } + + if !allUsers && len(args) < 2 { + return errors.New("you must specify some users to remove from the channel, or use the --all-users flag to remove them all") } channel := getChannelFromChannelArg(a, args[0]) @@ -207,9 +216,13 @@ func removeChannelUsersCmdF(command *cobra.Command, args []string) error { return errors.New("Unable to find channel '" + args[0] + "'") } - users := getUsersFromUserArgs(a, args[1:]) - for i, user := range users { - removeUserFromChannel(a, channel, user, args[i+1]) + if allUsers { + removeAllUsersFromChannel(a, channel) + } else { + users := getUsersFromUserArgs(a, args[1:]) + for i, user := range users { + removeUserFromChannel(a, channel, user, args[i+1]) + } } return nil @@ -225,6 +238,12 @@ func removeUserFromChannel(a *app.App, channel *model.Channel, user *model.User, } } +func removeAllUsersFromChannel(a *app.App, channel *model.Channel) { + if result := <-a.Srv.Store.Channel().PermanentDeleteMembersByChannel(channel.Id); result.Err != nil { + CommandPrintErrorln("Unable to remove all users from " + channel.Name + ". Error: " + result.Err.Error()) + } +} + func addChannelUsersCmdF(command *cobra.Command, args []string) error { a, err := InitDBCommandContextCobra(command) if err != nil { diff --git a/cmd/mattermost/commands/permissions.go b/cmd/mattermost/commands/permissions.go index 9d9962ce5..01d933a98 100644 --- a/cmd/mattermost/commands/permissions.go +++ b/cmd/mattermost/commands/permissions.go @@ -124,9 +124,5 @@ func importPermissionsCmdF(command *cobra.Command, args []string) error { } defer file.Close() - if err := a.ImportPermissions(file); err != nil { - return err - } - - return nil + return a.ImportPermissions(file) } diff --git a/cmd/mattermost/commands/permissions_test.go b/cmd/mattermost/commands/permissions_test.go index eeaa17109..54ccbddb8 100644 --- a/cmd/mattermost/commands/permissions_test.go +++ b/cmd/mattermost/commands/permissions_test.go @@ -30,7 +30,7 @@ func permissionsLicenseRequiredTest(t *testing.T, subcommand string) { t.Fail() } args := []string{"-test.run", "ExecCommand", "--", "--disableconfigwatch", "permissions", subcommand} - output, err := exec.Command(path, args...).CombinedOutput() + output, _ := exec.Command(path, args...).CombinedOutput() actual := string(output) expected := utils.T("cli.license.critical") diff --git a/config/default.json b/config/default.json index dc103638e..b303365b5 100644 --- a/config/default.json +++ b/config/default.json @@ -130,7 +130,8 @@ "MaxOpenConns": 300, "Trace": false, "AtRestEncryptKey": "", - "QueryTimeout": 30 + "QueryTimeout": 30, + "EnablePublicChannelsMaterialization": true }, "LogSettings": { "EnableConsole": true, diff --git a/i18n/en.json b/i18n/en.json index 0ad1722fc..0e1bdcf21 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -121,7 +121,7 @@ }, { "id": "api.channel.create_channel.max_channel_limit.app_error", - "translation": "Cannot create more than {{.MaxChannelsPerTeam}} channels for current team" + "translation": "Unable to create more than {{.MaxChannelsPerTeam}} channels for current team" }, { "id": "api.channel.create_default_channels.off_topic", @@ -149,7 +149,7 @@ }, { "id": "api.channel.delete_channel.cannot.app_error", - "translation": "Cannot delete the default channel {{.Channel}}" + "translation": "Unable to delete the default channel {{.Channel}}" }, { "id": "api.channel.delete_channel.deleted.app_error", @@ -157,7 +157,7 @@ }, { "id": "api.channel.delete_channel.type.invalid", - "translation": "Cannot delete direct or group message channels" + "translation": "Unable to delete direct or group message channels" }, { "id": "api.channel.join_channel.permissions.app_error", @@ -169,11 +169,11 @@ }, { "id": "api.channel.leave.default.app_error", - "translation": "Cannot leave the default channel {{.Channel}}" + "translation": "Unable to leave the default channel {{.Channel}}" }, { "id": "api.channel.leave.direct.app_error", - "translation": "Cannot leave a direct message channel" + "translation": "Unable to leave a direct message channel" }, { "id": "api.channel.leave.last_member.app_error", @@ -229,11 +229,11 @@ }, { "id": "api.channel.remove.default.app_error", - "translation": "Cannot remove user from the default channel {{.Channel}}" + "translation": "Unable to remove user from the default channel {{.Channel}}" }, { "id": "api.channel.remove_channel_member.type.app_error", - "translation": "Cannot remove user from a channel." + "translation": "Unable to remove user from a channel." }, { "id": "api.channel.remove_member.removed", @@ -337,7 +337,7 @@ }, { "id": "api.command.team_mismatch.app_error", - "translation": "Cannot update commands across teams" + "translation": "Unable to update commands across teams" }, { "id": "api.command_away.desc", @@ -389,7 +389,7 @@ }, { "id": "api.command_channel_purpose.direct_group.app_error", - "translation": "Cannot set purpose for direct message channels. Use /header to set the header instead." + "translation": "Unable to set purpose for direct message channels. Use /header to set the header instead." }, { "id": "api.command_channel_purpose.hint", @@ -421,7 +421,7 @@ }, { "id": "api.command_channel_rename.direct_group.app_error", - "translation": "Cannot rename direct message channels." + "translation": "Unable to rename direct message channels." }, { "id": "api.command_channel_rename.hint", @@ -558,8 +558,8 @@ { "id": "api.command_groupmsg.invalid_user.app_error", "translation": { - "one": "We couldn't find the user: {{.Users}}", - "other": "We couldn't find the users: {{.Users}}" + "one": "Unable to find the user: {{.Users}}", + "other": "Unable to find the users: {{.Users}}" } }, { @@ -616,7 +616,7 @@ }, { "id": "api.command_invite.missing_user.app_error", - "translation": "We couldn't find the user." + "translation": "Unable to find the user." }, { "id": "api.command_invite.name", @@ -660,7 +660,7 @@ }, { "id": "api.command_join.missing.app_error", - "translation": "We couldn't find the channel" + "translation": "Unable to find the channel" }, { "id": "api.command_join.name", @@ -728,7 +728,7 @@ }, { "id": "api.command_msg.missing.app_error", - "translation": "We couldn't find the user" + "translation": "Unable to find the user" }, { "id": "api.command_msg.name", @@ -824,7 +824,7 @@ }, { "id": "api.command_remove.missing.app_error", - "translation": "We couldn't find the user" + "translation": "Unable to find the user" }, { "id": "api.command_remove.name", @@ -1038,11 +1038,11 @@ }, { "id": "api.file.file_exists.exists_local.app_error", - "translation": "Unable to know if the file exists. An error ocurred when trying to check file existency." + "translation": "Unable to check if the file exists." }, { "id": "api.file.file_exists.s3.app_error", - "translation": "Unable to know if the file exists. An error ocurred when trying to check file existency." + "translation": "Unable to check if the file exists." }, { "id": "api.file.get_file.public_invalid.app_error", @@ -1408,7 +1408,7 @@ }, { "id": "api.post.update_post.find.app_error", - "translation": "We couldn't find the existing post or comment to update." + "translation": "Unable to find the existing post or comment to update." }, { "id": "api.post.update_post.permissions_details.app_error", @@ -1488,7 +1488,7 @@ }, { "id": "api.server.start_server.forward80to443.enabled_but_listening_on_wrong_port", - "translation": "Cannot forward port 80 to port 443 while listening on port %s: disable Forward80To443 if using a proxy server" + "translation": "Unable to forward port 80 to port 443 while listening on port %s: disable Forward80To443 if using a proxy server" }, { "id": "api.server.start_server.rate_limiting_memory_store", @@ -2196,7 +2196,7 @@ }, { "id": "api.user.reset_password.sso.app_error", - "translation": "Cannot reset password for SSO accounts" + "translation": "Unable to reset password for SSO accounts" }, { "id": "api.user.saml.app_error", @@ -2248,7 +2248,7 @@ }, { "id": "api.user.send_password_reset.sso.app_error", - "translation": "Cannot reset password for SSO accounts" + "translation": "Unable to reset password for SSO accounts" }, { "id": "api.user.send_sign_in_change_email_and_forget.error", @@ -2396,7 +2396,7 @@ }, { "id": "api.webhook.team_mismatch.app_error", - "translation": "Cannot update webhook across teams" + "translation": "Unable to update webhook across teams" }, { "id": "api.webhook.update_outgoing.intersect.app_error", @@ -2424,7 +2424,7 @@ }, { "id": "app.channel.move_channel.members_do_not_match.error", - "translation": "Cannot move a channel unless all its members are already members of the destination team." + "translation": "Unable to move a channel unless all its members are already members of the destination team." }, { "id": "app.channel.post_update_channel_purpose_message.post.error", @@ -2476,7 +2476,7 @@ }, { "id": "app.import.import_channel.scheme_deleted.error", - "translation": "Cannot set a channel to use a deleted scheme." + "translation": "Unable to set a channel to use a deleted scheme." }, { "id": "app.import.import_channel.scheme_wrong_scope.error", @@ -2580,7 +2580,7 @@ }, { "id": "app.import.import_team.scheme_deleted.error", - "translation": "Cannot set a team to use a deleted scheme." + "translation": "Unable to set a team to use a deleted scheme." }, { "id": "app.import.import_team.scheme_wrong_scope.error", @@ -3428,7 +3428,7 @@ }, { "id": "ent.message_export.global_relay.close_zip_file.app_error", - "translation": "Unable to close properly the zip file." + "translation": "Unable to close the zip file." }, { "id": "ent.message_export.global_relay.create_file_in_zip.app_error", @@ -3628,7 +3628,7 @@ }, { "id": "migrations.worker.run_migration.unknown_key", - "translation": "Cannot run migration job due to unknown migration key." + "translation": "Unable to run migration job due to unknown migration key." }, { "id": "model.access.is_valid.access_token.app_error", @@ -4748,11 +4748,11 @@ }, { "id": "store.sql_channel.analytics_deleted_type_count.app_error", - "translation": "We couldn't get deleted channel type counts" + "translation": "Unable to get deleted channel type counts" }, { "id": "store.sql_channel.analytics_type_count.app_error", - "translation": "We couldn't get channel type counts" + "translation": "Unable to get channel type counts" }, { "id": "store.sql_channel.clear_all_custom_role_assignments.commit_transaction.app_error", @@ -4776,11 +4776,11 @@ }, { "id": "store.sql_channel.delete.channel.app_error", - "translation": "We couldn't delete the channel" + "translation": "Unable to delete the channel" }, { "id": "store.sql_channel.get.existing.app_error", - "translation": "We couldn't find the existing channel" + "translation": "Unable to find the existing channel" }, { "id": "store.sql_channel.get.find.app_error", @@ -4788,11 +4788,11 @@ }, { "id": "store.sql_channel.get_all.app_error", - "translation": "We couldn't get all the channels" + "translation": "Unable to get all the channels" }, { "id": "store.sql_channel.get_by_name.existing.app_error", - "translation": "We couldn't find the existing channel" + "translation": "Unable to find the existing channel" }, { "id": "store.sql_channel.get_by_name.missing.app_error", @@ -4804,11 +4804,11 @@ }, { "id": "store.sql_channel.get_channel_counts.get.app_error", - "translation": "We couldn't get the channel counts" + "translation": "Unable to get the channel counts" }, { "id": "store.sql_channel.get_channels.get.app_error", - "translation": "We couldn't get the channels" + "translation": "Unable to get the channels" }, { "id": "store.sql_channel.get_channels.not_found.app_error", @@ -4816,7 +4816,7 @@ }, { "id": "store.sql_channel.get_channels_by_ids.get.app_error", - "translation": "We couldn't get the channels" + "translation": "Unable to get the channels" }, { "id": "store.sql_channel.get_channels_by_ids.not_found.app_error", @@ -4824,7 +4824,7 @@ }, { "id": "store.sql_channel.get_deleted.existing.app_error", - "translation": "We couldn't find the existing deleted channel" + "translation": "Unable to find the existing deleted channel" }, { "id": "store.sql_channel.get_deleted.missing.app_error", @@ -4832,7 +4832,7 @@ }, { "id": "store.sql_channel.get_deleted_by_name.existing.app_error", - "translation": "We couldn't find the existing deleted channel" + "translation": "Unable to find the existing deleted channel" }, { "id": "store.sql_channel.get_deleted_by_name.missing.app_error", @@ -4840,11 +4840,11 @@ }, { "id": "store.sql_channel.get_for_post.app_error", - "translation": "We couldn't get the channel for the given post" + "translation": "Unable to get the channel for the given post" }, { "id": "store.sql_channel.get_member.app_error", - "translation": "We couldn't get the channel member" + "translation": "Unable to get the channel member" }, { "id": "store.sql_channel.get_member.missing.app_error", @@ -4852,35 +4852,35 @@ }, { "id": "store.sql_channel.get_member_count.app_error", - "translation": "We couldn't get the channel member count" + "translation": "Unable to get the channel member count" }, { "id": "store.sql_channel.get_member_for_post.app_error", - "translation": "We couldn't get the channel member for the given post" + "translation": "Unable to get the channel member for the given post" }, { "id": "store.sql_channel.get_members.app_error", - "translation": "We couldn't get the channel members" + "translation": "Unable to get the channel members" }, { "id": "store.sql_channel.get_members_by_ids.app_error", - "translation": "We couldn't get the channel members" + "translation": "Unable to get the channel members" }, { "id": "store.sql_channel.get_more_channels.get.app_error", - "translation": "We couldn't get the channels" + "translation": "Unable to get the channels" }, { "id": "store.sql_channel.get_public_channels.get.app_error", - "translation": "We couldn't get public channels" + "translation": "Unable to get public channels" }, { "id": "store.sql_channel.get_unread.app_error", - "translation": "We couldn't get the channel unread messages" + "translation": "Unable to get the channel unread messages" }, { "id": "store.sql_channel.increment_mention_count.app_error", - "translation": "We couldn't increment the mention count" + "translation": "Unable to increment the mention count" }, { "id": "store.sql_channel.migrate_channel_members.commit_transaction.app_error", @@ -4904,23 +4904,23 @@ }, { "id": "store.sql_channel.permanent_delete.app_error", - "translation": "We couldn't delete the channel" + "translation": "Unable to delete the channel" }, { "id": "store.sql_channel.permanent_delete_by_team.app_error", - "translation": "We couldn't delete the channels" + "translation": "Unable to delete the channels" }, { "id": "store.sql_channel.permanent_delete_members_by_user.app_error", - "translation": "We couldn't remove the channel member" + "translation": "Unable to remove the channel member" }, { "id": "store.sql_channel.pinned_posts.app_error", - "translation": "We couldn't find the pinned posts" + "translation": "Unable to find the pinned posts" }, { "id": "store.sql_channel.remove_member.app_error", - "translation": "We couldn't remove the channel member" + "translation": "Unable to remove the channel member" }, { "id": "store.sql_channel.reset_all_channel_schemes.app_error", @@ -4968,7 +4968,7 @@ }, { "id": "store.sql_channel.save_channel.save.app_error", - "translation": "We couldn't save the channel" + "translation": "Unable to save the channel" }, { "id": "store.sql_channel.save_direct_channel.add_members.app_error", @@ -5000,7 +5000,7 @@ }, { "id": "store.sql_channel.save_member.save.app_error", - "translation": "We couldn't save the channel member" + "translation": "Unable to save the channel member" }, { "id": "store.sql_channel.search.app_error", @@ -5008,7 +5008,7 @@ }, { "id": "store.sql_channel.update.app_error", - "translation": "We couldn't update the channel" + "translation": "Unable to update the channel" }, { "id": "store.sql_channel.update.archived_channel.app_error", @@ -5028,7 +5028,7 @@ }, { "id": "store.sql_channel.update_last_viewed_at.app_error", - "translation": "We couldn't update the last viewed at time" + "translation": "Unable to update the last viewed at time" }, { "id": "store.sql_channel.update_member.app_error", @@ -5076,31 +5076,31 @@ }, { "id": "store.sql_command.analytics_command_count.app_error", - "translation": "We couldn't count the commands" + "translation": "Unable to count the commands" }, { "id": "store.sql_command.get_by_trigger.app_error", - "translation": "We couldn't get the command" + "translation": "Unable to get the command" }, { "id": "store.sql_command.save.delete.app_error", - "translation": "We couldn't delete the command" + "translation": "Unable to delete the command" }, { "id": "store.sql_command.save.delete_perm.app_error", - "translation": "We couldn't delete the command" + "translation": "Unable to delete the command" }, { "id": "store.sql_command.save.get.app_error", - "translation": "We couldn't get the command" + "translation": "Unable to get the command" }, { "id": "store.sql_command.save.get_team.app_error", - "translation": "We couldn't get the commands" + "translation": "Unable to get the commands" }, { "id": "store.sql_command.save.saving.app_error", - "translation": "We couldn't save the Command" + "translation": "Unable to save the Command" }, { "id": "store.sql_command.save.saving_overwrite.app_error", @@ -5108,15 +5108,15 @@ }, { "id": "store.sql_command.save.update.app_error", - "translation": "We couldn't update the command" + "translation": "Unable to update the command" }, { "id": "store.sql_command_webhooks.get.app_error", - "translation": "We couldn't get the webhook" + "translation": "Unable to get the webhook" }, { "id": "store.sql_command_webhooks.save.app_error", - "translation": "We couldn't save the CommandWebhook" + "translation": "Unable to save the CommandWebhook" }, { "id": "store.sql_command_webhooks.save.existing.app_error", @@ -5144,7 +5144,7 @@ }, { "id": "store.sql_emoji.delete.app_error", - "translation": "We couldn't delete the emoji" + "translation": "Unable to delete the emoji" }, { "id": "store.sql_emoji.delete.no_results", @@ -5152,51 +5152,51 @@ }, { "id": "store.sql_emoji.get.app_error", - "translation": "We couldn't get the emoji" + "translation": "Unable to get the emoji" }, { "id": "store.sql_emoji.get_all.app_error", - "translation": "We couldn't get the emoji" + "translation": "Unable to get the emoji" }, { "id": "store.sql_emoji.get_by_name.app_error", - "translation": "We couldn't get the emoji" + "translation": "Unable to get the emoji" }, { "id": "store.sql_emoji.save.app_error", - "translation": "We couldn't save the emoji" + "translation": "Unable to save the emoji" }, { "id": "store.sql_file_info.PermanentDeleteByUser.app_error", - "translation": "We couldn't delete attachments of the user" + "translation": "Unable to delete attachments of the user" }, { "id": "store.sql_file_info.attach_to_post.app_error", - "translation": "We couldn't attach the file info to the post" + "translation": "Unable to attach the file info to the post" }, { "id": "store.sql_file_info.delete_for_post.app_error", - "translation": "We couldn't delete the file info to the post" + "translation": "Unable to delete the file info to the post" }, { "id": "store.sql_file_info.get.app_error", - "translation": "We couldn't get the file info" + "translation": "Unable to get the file info" }, { "id": "store.sql_file_info.get_by_path.app_error", - "translation": "We couldn't get the file info by path" + "translation": "Unable to get the file info by path" }, { "id": "store.sql_file_info.get_for_post.app_error", - "translation": "We couldn't get the file info for the post" + "translation": "Unable to get the file info for the post" }, { "id": "store.sql_file_info.get_for_user_id.app_error", - "translation": "We couldn't get the file info for the user" + "translation": "Unable to get the file info for the user" }, { "id": "store.sql_file_info.permanent_delete.app_error", - "translation": "We couldn't permanently delete the file info" + "translation": "Unable to permanently delete the file info" }, { "id": "store.sql_file_info.permanent_delete_batch.app_error", @@ -5204,35 +5204,35 @@ }, { "id": "store.sql_file_info.save.app_error", - "translation": "We couldn't save the file info" + "translation": "Unable to save the file info" }, { "id": "store.sql_job.delete.app_error", - "translation": "We couldn't delete the job" + "translation": "Unable to delete the job" }, { "id": "store.sql_job.get.app_error", - "translation": "We couldn't get the job" + "translation": "Unable to get the job" }, { "id": "store.sql_job.get_all.app_error", - "translation": "We couldn't get the jobs" + "translation": "Unable to get the jobs" }, { "id": "store.sql_job.get_count_by_status_and_type.app_error", - "translation": "We couldn't get the job count by status and type" + "translation": "Unable to get the job count by status and type" }, { "id": "store.sql_job.get_newest_job_by_status_and_type.app_error", - "translation": "We couldn't get the newest job by status and type" + "translation": "Unable to get the newest job by status and type" }, { "id": "store.sql_job.save.app_error", - "translation": "We couldn't save the job" + "translation": "Unable to save the job" }, { "id": "store.sql_job.update.app_error", - "translation": "We couldn't update the job" + "translation": "Unable to update the job" }, { "id": "store.sql_license.get.app_error", @@ -5272,7 +5272,7 @@ }, { "id": "store.sql_oauth.get_app.find.app_error", - "translation": "We couldn't find the requested app" + "translation": "Unable to find the requested app" }, { "id": "store.sql_oauth.get_app.finding.app_error", @@ -5280,7 +5280,7 @@ }, { "id": "store.sql_oauth.get_app_by_user.find.app_error", - "translation": "We couldn't find any existing apps" + "translation": "Unable to find any existing apps" }, { "id": "store.sql_oauth.get_apps.find.app_error", @@ -5288,7 +5288,7 @@ }, { "id": "store.sql_oauth.get_auth_data.find.app_error", - "translation": "We couldn't find the existing authorization code" + "translation": "Unable to find the existing authorization code" }, { "id": "store.sql_oauth.get_auth_data.finding.app_error", @@ -5300,19 +5300,19 @@ }, { "id": "store.sql_oauth.permanent_delete_auth_data_by_user.app_error", - "translation": "We couldn't remove the authorization code" + "translation": "Unable to remove the authorization code" }, { "id": "store.sql_oauth.remove_access_data.app_error", - "translation": "We couldn't remove the access token" + "translation": "Unable to remove the access token" }, { "id": "store.sql_oauth.remove_auth_data.app_error", - "translation": "We couldn't remove the authorization code" + "translation": "Unable to remove the authorization code" }, { "id": "store.sql_oauth.save_access_data.app_error", - "translation": "We couldn't save the access token." + "translation": "Unable to save the access token." }, { "id": "store.sql_oauth.save_app.existing.app_error", @@ -5320,11 +5320,11 @@ }, { "id": "store.sql_oauth.save_app.save.app_error", - "translation": "We couldn't save the app." + "translation": "Unable to save the app." }, { "id": "store.sql_oauth.save_auth_data.app_error", - "translation": "We couldn't save the authorization code." + "translation": "Unable to save the authorization code." }, { "id": "store.sql_oauth.update_access_data.app_error", @@ -5332,7 +5332,7 @@ }, { "id": "store.sql_oauth.update_app.find.app_error", - "translation": "We couldn't find the existing app to update" + "translation": "Unable to find the existing app to update" }, { "id": "store.sql_oauth.update_app.finding.app_error", @@ -5340,7 +5340,7 @@ }, { "id": "store.sql_oauth.update_app.update.app_error", - "translation": "We couldn't update the app" + "translation": "Unable to update the app" }, { "id": "store.sql_oauth.update_app.updating.app_error", @@ -5360,35 +5360,35 @@ }, { "id": "store.sql_post.analytics_posts_count.app_error", - "translation": "We couldn't get post counts" + "translation": "Unable to get post counts" }, { "id": "store.sql_post.analytics_posts_count_by_day.app_error", - "translation": "We couldn't get post counts by day" + "translation": "Unable to get post counts by day" }, { "id": "store.sql_post.analytics_user_counts_posts_by_day.app_error", - "translation": "We couldn't get user counts with posts" + "translation": "Unable to get user counts with posts" }, { "id": "store.sql_post.compliance_export.app_error", - "translation": "We couldn't get the compliance export posts." + "translation": "Unable to get the compliance export posts." }, { "id": "store.sql_post.delete.app_error", - "translation": "We couldn't delete the post" + "translation": "Unable to delete the post" }, { "id": "store.sql_post.get.app_error", - "translation": "We couldn't get the post" + "translation": "Unable to get the post" }, { "id": "store.sql_post.get_flagged_posts.app_error", - "translation": "We couldn't get the flagged posts" + "translation": "Unable to get the flagged posts" }, { "id": "store.sql_post.get_parents_posts.app_error", - "translation": "We couldn't get the parent post for the channel" + "translation": "Unable to get the parent post for the channel" }, { "id": "store.sql_post.get_posts.app_error", @@ -5396,43 +5396,43 @@ }, { "id": "store.sql_post.get_posts_around.get.app_error", - "translation": "We couldn't get the posts for the channel" + "translation": "Unable to get the posts for the channel" }, { "id": "store.sql_post.get_posts_around.get_parent.app_error", - "translation": "We couldn't get the parent posts for the channel" + "translation": "Unable to get the parent posts for the channel" }, { "id": "store.sql_post.get_posts_batch_for_indexing.get.app_error", - "translation": "We couldn't get the posts batch for indexing" + "translation": "Unable to get the posts batch for indexing" }, { "id": "store.sql_post.get_posts_by_ids.app_error", - "translation": "We couldn't get the posts" + "translation": "Unable to get the posts" }, { "id": "store.sql_post.get_posts_created_att.app_error", - "translation": "We couldn't get the posts for the channel" + "translation": "Unable to get the posts for the channel" }, { "id": "store.sql_post.get_posts_since.app_error", - "translation": "We couldn't get the posts for the channel" + "translation": "Unable to get the posts for the channel" }, { "id": "store.sql_post.get_root_posts.app_error", - "translation": "We couldn't get the posts for the channel" + "translation": "Unable to get the posts for the channel" }, { "id": "store.sql_post.overwrite.app_error", - "translation": "We couldn't overwrite the Post" + "translation": "Unable to overwrite the Post" }, { "id": "store.sql_post.permanent_delete.app_error", - "translation": "We couldn't delete the post" + "translation": "Unable to delete the post" }, { "id": "store.sql_post.permanent_delete_all_comments_by_user.app_error", - "translation": "We couldn't delete the comments for user" + "translation": "Unable to delete the comments for user" }, { "id": "store.sql_post.permanent_delete_batch.app_error", @@ -5440,23 +5440,23 @@ }, { "id": "store.sql_post.permanent_delete_by_channel.app_error", - "translation": "We couldn't delete the posts by channel" + "translation": "Unable to delete the posts by channel" }, { "id": "store.sql_post.permanent_delete_by_user.app_error", - "translation": "We couldn't select the posts to delete for the user" + "translation": "Unable to select the posts to delete for the user" }, { "id": "store.sql_post.permanent_delete_by_user.too_many.app_error", - "translation": "We couldn't select the posts to delete for the user (too many), please re-run" + "translation": "Unable to select the posts to delete for the user (too many), please re-run" }, { "id": "store.sql_post.query_max_post_size.error", - "translation": "We couldn't determine the maximum supported post size" + "translation": "Unable to determine the maximum supported post size" }, { "id": "store.sql_post.save.app_error", - "translation": "We couldn't save the Post" + "translation": "Unable to save the Post" }, { "id": "store.sql_post.save.existing.app_error", @@ -5468,7 +5468,7 @@ }, { "id": "store.sql_post.update.app_error", - "translation": "We couldn't update the Post" + "translation": "Unable to update the Post" }, { "id": "store.sql_preference.cleanup_flags_batch.app_error", @@ -5496,7 +5496,7 @@ }, { "id": "store.sql_preference.insert.save.app_error", - "translation": "We couldn't save the preference" + "translation": "Unable to save the preference" }, { "id": "store.sql_preference.is_feature_enabled.app_error", @@ -5528,7 +5528,7 @@ }, { "id": "store.sql_preference.update.app_error", - "translation": "We couldn't update the preference" + "translation": "Unable to update the preference" }, { "id": "store.sql_reaction.delete.app_error", @@ -5672,7 +5672,7 @@ }, { "id": "store.sql_session.analytics_session_count.app_error", - "translation": "We couldn't count the sessions" + "translation": "Unable to count the sessions" }, { "id": "store.sql_session.get.app_error", @@ -5684,35 +5684,35 @@ }, { "id": "store.sql_session.permanent_delete_sessions_by_user.app_error", - "translation": "We couldn't remove all the sessions for the user" + "translation": "Unable to remove all the sessions for the user" }, { "id": "store.sql_session.remove.app_error", - "translation": "We couldn't remove the session" + "translation": "Unable to remove the session" }, { "id": "store.sql_session.remove_all_sessions_for_team.app_error", - "translation": "We couldn't remove all the sessions" + "translation": "Unable to remove all the sessions" }, { "id": "store.sql_session.save.app_error", - "translation": "We couldn't save the session" + "translation": "Unable to save the session" }, { "id": "store.sql_session.save.existing.app_error", - "translation": "Cannot update existing session" + "translation": "Unable to update existing session" }, { "id": "store.sql_session.update_device_id.app_error", - "translation": "We couldn't update the device id" + "translation": "Unable to update the device id" }, { "id": "store.sql_session.update_last_activity.app_error", - "translation": "We couldn't update the last_activity_at" + "translation": "Unable to update the last_activity_at" }, { "id": "store.sql_session.update_roles.app_error", - "translation": "We couldn't update the roles" + "translation": "Unable to update the roles" }, { "id": "store.sql_status.get.app_error", @@ -5760,7 +5760,7 @@ }, { "id": "store.sql_system.get_by_name.app_error", - "translation": "We couldn't find the system variable." + "translation": "Unable to find the system variable." }, { "id": "store.sql_system.permanent_delete_by_name.app_error", @@ -5776,11 +5776,11 @@ }, { "id": "store.sql_team.analytics_get_team_count_for_scheme.app_error", - "translation": "We couldn't get the channel count for the scheme." + "translation": "Unable to get the channel count for the scheme." }, { "id": "store.sql_team.analytics_team_count.app_error", - "translation": "We couldn't count the teams" + "translation": "Unable to count the teams" }, { "id": "store.sql_team.clear_all_custom_role_assignments.commit_transaction.app_error", @@ -5804,7 +5804,7 @@ }, { "id": "store.sql_team.get.find.app_error", - "translation": "We couldn't find the existing team" + "translation": "Unable to find the existing team" }, { "id": "store.sql_team.get.finding.app_error", @@ -5820,15 +5820,15 @@ }, { "id": "store.sql_team.get_by_invite_id.find.app_error", - "translation": "We couldn't find the existing team" + "translation": "Unable to find the existing team" }, { "id": "store.sql_team.get_by_invite_id.finding.app_error", - "translation": "We couldn't find the existing team" + "translation": "Unable to find the existing team" }, { "id": "store.sql_team.get_by_name.app_error", - "translation": "We couldn't find the existing team" + "translation": "Unable to find the existing team" }, { "id": "store.sql_team.get_by_scheme.app_error", @@ -5836,7 +5836,7 @@ }, { "id": "store.sql_team.get_member.app_error", - "translation": "We couldn't get the team member" + "translation": "Unable to get the team member" }, { "id": "store.sql_team.get_member.missing.app_error", @@ -5844,19 +5844,19 @@ }, { "id": "store.sql_team.get_member_count.app_error", - "translation": "We couldn't count the team members" + "translation": "Unable to count the team members" }, { "id": "store.sql_team.get_members.app_error", - "translation": "We couldn't get the team members" + "translation": "Unable to get the team members" }, { "id": "store.sql_team.get_members_by_ids.app_error", - "translation": "We couldn't get the team members" + "translation": "Unable to get the team members" }, { "id": "store.sql_team.get_unread.app_error", - "translation": "We couldn't get the teams unread messages" + "translation": "Unable to get the teams unread messages" }, { "id": "store.sql_team.migrate_team_members.commit_transaction.app_error", @@ -5880,11 +5880,11 @@ }, { "id": "store.sql_team.permanent_delete.app_error", - "translation": "We couldn't delete the existing team" + "translation": "Unable to delete the existing team" }, { "id": "store.sql_team.remove_member.app_error", - "translation": "We couldn't remove the team member" + "translation": "Unable to remove the team member" }, { "id": "store.sql_team.reset_all_team_schemes.app_error", @@ -5892,7 +5892,7 @@ }, { "id": "store.sql_team.save.app_error", - "translation": "We couldn't save the team" + "translation": "Unable to save the team" }, { "id": "store.sql_team.save.domain_exists.app_error", @@ -5908,7 +5908,7 @@ }, { "id": "store.sql_team.save_member.save.app_error", - "translation": "We couldn't save the team member" + "translation": "Unable to save the team member" }, { "id": "store.sql_team.search_all_team.app_error", @@ -5920,11 +5920,11 @@ }, { "id": "store.sql_team.update.app_error", - "translation": "We couldn't update the team" + "translation": "Unable to update the team" }, { "id": "store.sql_team.update.find.app_error", - "translation": "We couldn't find the existing team to update" + "translation": "Unable to find the existing team to update" }, { "id": "store.sql_team.update.finding.app_error", @@ -5936,15 +5936,15 @@ }, { "id": "store.sql_team.update_display_name.app_error", - "translation": "We couldn't update the team name" + "translation": "Unable to update the team name" }, { "id": "store.sql_team.update_last_team_icon_update.app_error", - "translation": "We couldn't update the date of the last team icon update" + "translation": "Unable to update the date of the last team icon update" }, { "id": "store.sql_user.analytics_daily_active_users.app_error", - "translation": "We couldn't get the active users during the requested period" + "translation": "Unable to get the active users during the requested period" }, { "id": "store.sql_user.analytics_get_inactive_users_count.app_error", @@ -5952,11 +5952,11 @@ }, { "id": "store.sql_user.analytics_get_system_admin_count.app_error", - "translation": "We couldn't get the system admin count" + "translation": "Unable to get the system admin count" }, { "id": "store.sql_user.analytics_unique_user_count.app_error", - "translation": "We couldn't get the unique user count" + "translation": "Unable to get the unique user count" }, { "id": "store.sql_user.clear_all_custom_role_assignments.commit_transaction.app_error", @@ -5984,7 +5984,7 @@ }, { "id": "store.sql_user.get_by_auth.missing_account.app_error", - "translation": "We couldn't find an existing account matching your authentication type for this team. This team may require an invite from the team owner to join." + "translation": "Unable to find an existing account matching your authentication type for this team. This team may require an invite from the team owner to join." }, { "id": "store.sql_user.get_by_auth.other.app_error", @@ -5992,11 +5992,11 @@ }, { "id": "store.sql_user.get_by_username.app_error", - "translation": "We couldn't find an existing account matching your username for this team. This team may require an invite from the team owner to join." + "translation": "Unable to find an existing account matching your username for this team. This team may require an invite from the team owner to join." }, { "id": "store.sql_user.get_for_login.app_error", - "translation": "We couldn't find an existing account matching your credentials. This team may require an invite from the team owner to join." + "translation": "Unable to find an existing account matching your credentials. This team may require an invite from the team owner to join." }, { "id": "store.sql_user.get_for_login.multiple_users", @@ -6036,15 +6036,15 @@ }, { "id": "store.sql_user.missing_account.const", - "translation": "We couldn't find the user." + "translation": "Unable to find the user." }, { "id": "store.sql_user.permanent_delete.app_error", - "translation": "We couldn't delete the existing account" + "translation": "Unable to delete the existing account" }, { "id": "store.sql_user.save.app_error", - "translation": "We couldn't save the account." + "translation": "Unable to save the account." }, { "id": "store.sql_user.save.email_exists.app_error", @@ -6084,11 +6084,11 @@ }, { "id": "store.sql_user.search.app_error", - "translation": "We couldn't find any user maching the search parameters" + "translation": "Unable to find any user maching the search parameters" }, { "id": "store.sql_user.update.app_error", - "translation": "We couldn't update the account" + "translation": "Unable to update the account" }, { "id": "store.sql_user.update.can_not_change_ldap.app_error", @@ -6100,7 +6100,7 @@ }, { "id": "store.sql_user.update.find.app_error", - "translation": "We couldn't find the existing account to update" + "translation": "Unable to find the existing account to update" }, { "id": "store.sql_user.update.finding.app_error", @@ -6116,7 +6116,7 @@ }, { "id": "store.sql_user.update_auth_data.app_error", - "translation": "We couldn't update the auth data" + "translation": "Unable to update the auth data" }, { "id": "store.sql_user.update_auth_data.email_exists.app_error", @@ -6124,11 +6124,11 @@ }, { "id": "store.sql_user.update_failed_pwd_attempts.app_error", - "translation": "We couldn't update the failed_attempts" + "translation": "Unable to update the failed_attempts" }, { "id": "store.sql_user.update_last_picture_update.app_error", - "translation": "We couldn't update the update_at" + "translation": "Unable to update the update_at" }, { "id": "store.sql_user.update_mfa_active.app_error", @@ -6140,11 +6140,11 @@ }, { "id": "store.sql_user.update_password.app_error", - "translation": "We couldn't update the user password" + "translation": "Unable to update the user password" }, { "id": "store.sql_user.update_update.app_error", - "translation": "We couldn't update the date of the last update of the user" + "translation": "Unable to update the date of the last update of the user" }, { "id": "store.sql_user.verify_email.app_error", @@ -6152,27 +6152,27 @@ }, { "id": "store.sql_user_access_token.delete.app_error", - "translation": "We couldn't delete the personal access token" + "translation": "Unable to delete the personal access token" }, { "id": "store.sql_user_access_token.get.app_error", - "translation": "We couldn't get the personal access token" + "translation": "Unable to get the personal access token" }, { "id": "store.sql_user_access_token.get_all.app_error", - "translation": "We couldn't get all personal access tokens" + "translation": "Unable to get all personal access tokens" }, { "id": "store.sql_user_access_token.get_by_token.app_error", - "translation": "We couldn't get the personal access token by token" + "translation": "Unable to get the personal access token by token" }, { "id": "store.sql_user_access_token.get_by_user.app_error", - "translation": "We couldn't get the personal access tokens by user" + "translation": "Unable to get the personal access tokens by user" }, { "id": "store.sql_user_access_token.save.app_error", - "translation": "We couldn't save the personal access token" + "translation": "Unable to save the personal access token" }, { "id": "store.sql_user_access_token.search.app_error", @@ -6180,71 +6180,71 @@ }, { "id": "store.sql_user_access_token.update_token_disable.app_error", - "translation": "We couldn't disable the access token" + "translation": "Unable to disable the access token" }, { "id": "store.sql_user_access_token.update_token_enable.app_error", - "translation": "We couldn't enable the access token" + "translation": "Unable to enable the access token" }, { "id": "store.sql_webhooks.analytics_incoming_count.app_error", - "translation": "We couldn't count the incoming webhooks" + "translation": "Unable to count the incoming webhooks" }, { "id": "store.sql_webhooks.analytics_outgoing_count.app_error", - "translation": "We couldn't count the outgoing webhooks" + "translation": "Unable to count the outgoing webhooks" }, { "id": "store.sql_webhooks.delete_incoming.app_error", - "translation": "We couldn't delete the webhook" + "translation": "Unable to delete the webhook" }, { "id": "store.sql_webhooks.delete_outgoing.app_error", - "translation": "We couldn't delete the webhook" + "translation": "Unable to delete the webhook" }, { "id": "store.sql_webhooks.get_incoming.app_error", - "translation": "We couldn't get the webhook" + "translation": "Unable to get the webhook" }, { "id": "store.sql_webhooks.get_incoming_by_channel.app_error", - "translation": "We couldn't get the webhooks" + "translation": "Unable to get the webhooks" }, { "id": "store.sql_webhooks.get_incoming_by_user.app_error", - "translation": "We couldn't get the webhook" + "translation": "Unable to get the webhook" }, { "id": "store.sql_webhooks.get_outgoing.app_error", - "translation": "We couldn't get the webhook" + "translation": "Unable to get the webhook" }, { "id": "store.sql_webhooks.get_outgoing_by_channel.app_error", - "translation": "We couldn't get the webhooks" + "translation": "Unable to get the webhooks" }, { "id": "store.sql_webhooks.get_outgoing_by_team.app_error", - "translation": "We couldn't get the webhooks" + "translation": "Unable to get the webhooks" }, { "id": "store.sql_webhooks.permanent_delete_incoming_by_channel.app_error", - "translation": "We couldn't delete the webhook" + "translation": "Unable to delete the webhook" }, { "id": "store.sql_webhooks.permanent_delete_incoming_by_user.app_error", - "translation": "We couldn't delete the webhook" + "translation": "Unable to delete the webhook" }, { "id": "store.sql_webhooks.permanent_delete_outgoing_by_channel.app_error", - "translation": "We couldn't delete the webhook" + "translation": "Unable to delete the webhook" }, { "id": "store.sql_webhooks.permanent_delete_outgoing_by_user.app_error", - "translation": "We couldn't delete the webhook" + "translation": "Unable to delete the webhook" }, { "id": "store.sql_webhooks.save_incoming.app_error", - "translation": "We couldn't save the IncomingWebhook" + "translation": "Unable to save the IncomingWebhook" }, { "id": "store.sql_webhooks.save_incoming.existing.app_error", @@ -6252,7 +6252,7 @@ }, { "id": "store.sql_webhooks.save_outgoing.app_error", - "translation": "We couldn't save the OutgoingWebhook" + "translation": "Unable to save the OutgoingWebhook" }, { "id": "store.sql_webhooks.save_outgoing.override.app_error", @@ -6260,11 +6260,11 @@ }, { "id": "store.sql_webhooks.update_incoming.app_error", - "translation": "We couldn't update the IncomingWebhook" + "translation": "Unable to update the IncomingWebhook" }, { "id": "store.sql_webhooks.update_outgoing.app_error", - "translation": "We couldn't update the webhook" + "translation": "Unable to update the webhook" }, { "id": "system.message.name", @@ -6384,7 +6384,7 @@ }, { "id": "web.get_access_token.internal_saving.app_error", - "translation": "We couldn't update the user access data." + "translation": "Unable to update the user access data." }, { "id": "web.incoming_webhook.channel.app_error", diff --git a/model/client4.go b/model/client4.go index 47d227742..c95aaad0b 100644 --- a/model/client4.go +++ b/model/client4.go @@ -1961,6 +1961,17 @@ func (c *Client4) AutocompleteChannelsForTeam(teamId, name string) (*ChannelList } } +// AutocompleteChannelsForTeamForSearch will return an ordered list of your channels autocomplete suggestions +func (c *Client4) AutocompleteChannelsForTeamForSearch(teamId, name string) (*ChannelList, *Response) { + query := fmt.Sprintf("?name=%v", name) + if r, err := c.DoApiGet(c.GetChannelsForTeamRoute(teamId)+"/search_autocomplete"+query, ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return ChannelListFromJson(r.Body), BuildResponse(r) + } +} + // Post Section // CreatePost creates a post based on the provided post struct. diff --git a/model/config.go b/model/config.go index c0f443b72..db3030170 100644 --- a/model/config.go +++ b/model/config.go @@ -644,16 +644,17 @@ type SSOSettings struct { } type SqlSettings struct { - DriverName *string - DataSource *string - DataSourceReplicas []string - DataSourceSearchReplicas []string - MaxIdleConns *int - ConnMaxLifetimeMilliseconds *int - MaxOpenConns *int - Trace bool - AtRestEncryptKey string - QueryTimeout *int + DriverName *string + DataSource *string + DataSourceReplicas []string + DataSourceSearchReplicas []string + MaxIdleConns *int + ConnMaxLifetimeMilliseconds *int + MaxOpenConns *int + Trace bool + AtRestEncryptKey string + QueryTimeout *int + EnablePublicChannelsMaterialization *bool } func (s *SqlSettings) SetDefaults() { @@ -684,6 +685,10 @@ func (s *SqlSettings) SetDefaults() { if s.QueryTimeout == nil { s.QueryTimeout = NewInt(30) } + + if s.EnablePublicChannelsMaterialization == nil { + s.EnablePublicChannelsMaterialization = NewBool(true) + } } type LogSettings struct { @@ -2370,7 +2375,7 @@ func (ss *ServiceSettings) isValid() *AppError { } } - host, port, err := net.SplitHostPort(*ss.ListenAddress) + host, port, _ := net.SplitHostPort(*ss.ListenAddress) var isValidHost bool if host == "" { isValidHost = true diff --git a/model/incoming_webhook.go b/model/incoming_webhook.go index 1d6d7b4f0..3856d22ff 100644 --- a/model/incoming_webhook.go +++ b/model/incoming_webhook.go @@ -93,7 +93,7 @@ func (o *IncomingWebhook) IsValid() *AppError { return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.display_name.app_error", nil, "", http.StatusBadRequest) } - if len(o.Description) > 128 { + if len(o.Description) > 500 { return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.description.app_error", nil, "", http.StatusBadRequest) } diff --git a/model/incoming_webhook_test.go b/model/incoming_webhook_test.go index 5498a6a0c..3f7d13695 100644 --- a/model/incoming_webhook_test.go +++ b/model/incoming_webhook_test.go @@ -80,12 +80,12 @@ func TestIncomingWebhookIsValid(t *testing.T) { t.Fatal(err) } - o.Description = strings.Repeat("1", 129) + o.Description = strings.Repeat("1", 501) if err := o.IsValid(); err == nil { t.Fatal("should be invalid") } - o.Description = strings.Repeat("1", 128) + o.Description = strings.Repeat("1", 500) if err := o.IsValid(); err != nil { t.Fatal(err) } diff --git a/model/outgoing_webhook.go b/model/outgoing_webhook.go index 698a226e3..5f7a67d04 100644 --- a/model/outgoing_webhook.go +++ b/model/outgoing_webhook.go @@ -171,7 +171,7 @@ func (o *OutgoingWebhook) IsValid() *AppError { return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.display_name.app_error", nil, "", http.StatusBadRequest) } - if len(o.Description) > 128 { + if len(o.Description) > 500 { return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.description.app_error", nil, "", http.StatusBadRequest) } diff --git a/model/outgoing_webhook_test.go b/model/outgoing_webhook_test.go index 3241e649f..5403fca6f 100644 --- a/model/outgoing_webhook_test.go +++ b/model/outgoing_webhook_test.go @@ -102,14 +102,14 @@ func TestOutgoingWebhookIsValid(t *testing.T) { t.Fatal(err) } - o.Description = strings.Repeat("1", 129) + o.Description = strings.Repeat("1", 501) if err := o.IsValid(); err == nil { t.Fatal("should be invalid") } - o.Description = strings.Repeat("1", 128) + o.Description = strings.Repeat("1", 500) if err := o.IsValid(); err != nil { - t.Fatal("should be invalid") + t.Fatal(err) } o.ContentType = strings.Repeat("1", 129) diff --git a/model/session_test.go b/model/session_test.go index bf32d2f09..88e0bdd43 100644 --- a/model/session_test.go +++ b/model/session_test.go @@ -31,7 +31,7 @@ func TestSessionDeepCopy(t *testing.T) { session = &Session{Id: sessionId} copySession = session.DeepCopy() - assert.Equal(t, sessionId, session.Id) + assert.Equal(t, sessionId, copySession.Id) session = &Session{TeamMembers: []*TeamMember{}} copySession = session.DeepCopy() diff --git a/model/user_test.go b/model/user_test.go index f86b52919..d7f7b6711 100644 --- a/model/user_test.go +++ b/model/user_test.go @@ -52,7 +52,7 @@ func TestUserDeepCopy(t *testing.T) { user = &User{Id: id} copyUser = user.DeepCopy() - assert.Equal(t, id, user.Id) + assert.Equal(t, id, copyUser.Id) } func TestUserJson(t *testing.T) { diff --git a/plugin/client_rpc.go b/plugin/client_rpc.go index dde4c5f2e..72bd41f68 100644 --- a/plugin/client_rpc.go +++ b/plugin/client_rpc.go @@ -62,12 +62,39 @@ type apiRPCServer struct { impl API } +// ErrorString is a fallback for sending unregistered implementations of the error interface across +// rpc. For example, the errorString type from the github.com/pkg/errors package cannot be +// registered since it is not exported, but this precludes common error handling paradigms. +// ErrorString merely preserves the string description of the error, while satisfying the error +// interface itself to allow other registered types (such as model.AppError) to be sent unmodified. +type ErrorString struct { + Err string +} + +func (e ErrorString) Error() string { + return e.Err +} + +func encodableError(err error) error { + if err == nil { + return nil + } + if _, ok := err.(*model.AppError); ok { + return err + } + + return &ErrorString{ + Err: err.Error(), + } +} + // Registering some types used by MM for encoding/gob used by rpc func init() { gob.Register([]*model.SlackAttachment{}) gob.Register([]interface{}{}) gob.Register(map[string]interface{}{}) gob.Register(&model.AppError{}) + gob.Register(&ErrorString{}) } // These enforce compile time checks to make sure types implement the interface @@ -128,7 +155,7 @@ func (s *hooksRPCServer) Implemented(args struct{}, reply *[]string) error { methods = append(methods, method.Name) } *reply = methods - return nil + return encodableError(nil) } type Z_OnActivateArgs struct { @@ -182,7 +209,7 @@ func (s *hooksRPCServer) OnActivate(args *Z_OnActivateArgs, returns *Z_OnActivat if hook, ok := s.impl.(interface { OnActivate() error }); ok { - returns.A = hook.OnActivate() + returns.A = encodableError(hook.OnActivate()) } return nil } @@ -265,7 +292,7 @@ func (g *hooksRPCClient) ServeHTTP(c *Context, w http.ResponseWriter, r *http.Re go func() { bodyConnection, err := g.muxBroker.Accept(requestBodyStreamId) if err != nil { - g.log.Error("Plugin failed to ServeHTTP, muxBroker couldn't Accept request body connecion", mlog.Err(err)) + g.log.Error("Plugin failed to ServeHTTP, muxBroker couldn't Accept request body connection", mlog.Err(err)) http.Error(w, "500 internal server error", http.StatusInternalServerError) return } @@ -295,7 +322,6 @@ func (g *hooksRPCClient) ServeHTTP(c *Context, w http.ResponseWriter, r *http.Re g.log.Error("Plugin failed to ServeHTTP, RPC call failed", mlog.Err(err)) http.Error(w, "500 internal server error", http.StatusInternalServerError) } - return } func (s *hooksRPCServer) ServeHTTP(args *Z_ServeHTTPArgs, returns *struct{}) error { diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index d0d576f8d..1403e3ff2 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -41,8 +41,9 @@ func (s *hooksRPCServer) OnDeactivate(args *Z_OnDeactivateArgs, returns *Z_OnDea OnDeactivate() error }); ok { returns.A = hook.OnDeactivate() + returns.A = encodableError(returns.A) } else { - return fmt.Errorf("Hook OnDeactivate called but not implemented.") + return encodableError(fmt.Errorf("Hook OnDeactivate called but not implemented.")) } return nil } @@ -74,8 +75,9 @@ func (s *hooksRPCServer) OnConfigurationChange(args *Z_OnConfigurationChangeArgs OnConfigurationChange() error }); ok { returns.A = hook.OnConfigurationChange() + returns.A = encodableError(returns.A) } else { - return fmt.Errorf("Hook OnConfigurationChange called but not implemented.") + return encodableError(fmt.Errorf("Hook OnConfigurationChange called but not implemented.")) } return nil } @@ -110,8 +112,9 @@ func (s *hooksRPCServer) ExecuteCommand(args *Z_ExecuteCommandArgs, returns *Z_E ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) }); ok { returns.A, returns.B = hook.ExecuteCommand(args.A, args.B) + } else { - return fmt.Errorf("Hook ExecuteCommand called but not implemented.") + return encodableError(fmt.Errorf("Hook ExecuteCommand called but not implemented.")) } return nil } @@ -146,8 +149,9 @@ func (s *hooksRPCServer) MessageWillBePosted(args *Z_MessageWillBePostedArgs, re MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string) }); ok { returns.A, returns.B = hook.MessageWillBePosted(args.A, args.B) + } else { - return fmt.Errorf("Hook MessageWillBePosted called but not implemented.") + return encodableError(fmt.Errorf("Hook MessageWillBePosted called but not implemented.")) } return nil } @@ -183,8 +187,9 @@ func (s *hooksRPCServer) MessageWillBeUpdated(args *Z_MessageWillBeUpdatedArgs, MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string) }); ok { returns.A, returns.B = hook.MessageWillBeUpdated(args.A, args.B, args.C) + } else { - return fmt.Errorf("Hook MessageWillBeUpdated called but not implemented.") + return encodableError(fmt.Errorf("Hook MessageWillBeUpdated called but not implemented.")) } return nil } @@ -209,7 +214,7 @@ func (g *hooksRPCClient) MessageHasBeenPosted(c *Context, post *model.Post) { g.log.Error("RPC call MessageHasBeenPosted to plugin failed.", mlog.Err(err)) } } - return + } func (s *hooksRPCServer) MessageHasBeenPosted(args *Z_MessageHasBeenPostedArgs, returns *Z_MessageHasBeenPostedReturns) error { @@ -217,8 +222,9 @@ func (s *hooksRPCServer) MessageHasBeenPosted(args *Z_MessageHasBeenPostedArgs, MessageHasBeenPosted(c *Context, post *model.Post) }); ok { hook.MessageHasBeenPosted(args.A, args.B) + } else { - return fmt.Errorf("Hook MessageHasBeenPosted called but not implemented.") + return encodableError(fmt.Errorf("Hook MessageHasBeenPosted called but not implemented.")) } return nil } @@ -244,7 +250,7 @@ func (g *hooksRPCClient) MessageHasBeenUpdated(c *Context, newPost, oldPost *mod g.log.Error("RPC call MessageHasBeenUpdated to plugin failed.", mlog.Err(err)) } } - return + } func (s *hooksRPCServer) MessageHasBeenUpdated(args *Z_MessageHasBeenUpdatedArgs, returns *Z_MessageHasBeenUpdatedReturns) error { @@ -252,8 +258,9 @@ func (s *hooksRPCServer) MessageHasBeenUpdated(args *Z_MessageHasBeenUpdatedArgs MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post) }); ok { hook.MessageHasBeenUpdated(args.A, args.B, args.C) + } else { - return fmt.Errorf("Hook MessageHasBeenUpdated called but not implemented.") + return encodableError(fmt.Errorf("Hook MessageHasBeenUpdated called but not implemented.")) } return nil } @@ -278,7 +285,7 @@ func (g *hooksRPCClient) ChannelHasBeenCreated(c *Context, channel *model.Channe g.log.Error("RPC call ChannelHasBeenCreated to plugin failed.", mlog.Err(err)) } } - return + } func (s *hooksRPCServer) ChannelHasBeenCreated(args *Z_ChannelHasBeenCreatedArgs, returns *Z_ChannelHasBeenCreatedReturns) error { @@ -286,8 +293,9 @@ func (s *hooksRPCServer) ChannelHasBeenCreated(args *Z_ChannelHasBeenCreatedArgs ChannelHasBeenCreated(c *Context, channel *model.Channel) }); ok { hook.ChannelHasBeenCreated(args.A, args.B) + } else { - return fmt.Errorf("Hook ChannelHasBeenCreated called but not implemented.") + return encodableError(fmt.Errorf("Hook ChannelHasBeenCreated called but not implemented.")) } return nil } @@ -313,7 +321,7 @@ func (g *hooksRPCClient) UserHasJoinedChannel(c *Context, channelMember *model.C g.log.Error("RPC call UserHasJoinedChannel to plugin failed.", mlog.Err(err)) } } - return + } func (s *hooksRPCServer) UserHasJoinedChannel(args *Z_UserHasJoinedChannelArgs, returns *Z_UserHasJoinedChannelReturns) error { @@ -321,8 +329,9 @@ func (s *hooksRPCServer) UserHasJoinedChannel(args *Z_UserHasJoinedChannelArgs, UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) }); ok { hook.UserHasJoinedChannel(args.A, args.B, args.C) + } else { - return fmt.Errorf("Hook UserHasJoinedChannel called but not implemented.") + return encodableError(fmt.Errorf("Hook UserHasJoinedChannel called but not implemented.")) } return nil } @@ -348,7 +357,7 @@ func (g *hooksRPCClient) UserHasLeftChannel(c *Context, channelMember *model.Cha g.log.Error("RPC call UserHasLeftChannel to plugin failed.", mlog.Err(err)) } } - return + } func (s *hooksRPCServer) UserHasLeftChannel(args *Z_UserHasLeftChannelArgs, returns *Z_UserHasLeftChannelReturns) error { @@ -356,8 +365,9 @@ func (s *hooksRPCServer) UserHasLeftChannel(args *Z_UserHasLeftChannelArgs, retu UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) }); ok { hook.UserHasLeftChannel(args.A, args.B, args.C) + } else { - return fmt.Errorf("Hook UserHasLeftChannel called but not implemented.") + return encodableError(fmt.Errorf("Hook UserHasLeftChannel called but not implemented.")) } return nil } @@ -383,7 +393,7 @@ func (g *hooksRPCClient) UserHasJoinedTeam(c *Context, teamMember *model.TeamMem g.log.Error("RPC call UserHasJoinedTeam to plugin failed.", mlog.Err(err)) } } - return + } func (s *hooksRPCServer) UserHasJoinedTeam(args *Z_UserHasJoinedTeamArgs, returns *Z_UserHasJoinedTeamReturns) error { @@ -391,8 +401,9 @@ func (s *hooksRPCServer) UserHasJoinedTeam(args *Z_UserHasJoinedTeamArgs, return UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User) }); ok { hook.UserHasJoinedTeam(args.A, args.B, args.C) + } else { - return fmt.Errorf("Hook UserHasJoinedTeam called but not implemented.") + return encodableError(fmt.Errorf("Hook UserHasJoinedTeam called but not implemented.")) } return nil } @@ -418,7 +429,7 @@ func (g *hooksRPCClient) UserHasLeftTeam(c *Context, teamMember *model.TeamMembe g.log.Error("RPC call UserHasLeftTeam to plugin failed.", mlog.Err(err)) } } - return + } func (s *hooksRPCServer) UserHasLeftTeam(args *Z_UserHasLeftTeamArgs, returns *Z_UserHasLeftTeamReturns) error { @@ -426,8 +437,9 @@ func (s *hooksRPCServer) UserHasLeftTeam(args *Z_UserHasLeftTeamArgs, returns *Z UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User) }); ok { hook.UserHasLeftTeam(args.A, args.B, args.C) + } else { - return fmt.Errorf("Hook UserHasLeftTeam called but not implemented.") + return encodableError(fmt.Errorf("Hook UserHasLeftTeam called but not implemented.")) } return nil } @@ -461,8 +473,9 @@ func (s *hooksRPCServer) UserWillLogIn(args *Z_UserWillLogInArgs, returns *Z_Use UserWillLogIn(c *Context, user *model.User) string }); ok { returns.A = hook.UserWillLogIn(args.A, args.B) + } else { - return fmt.Errorf("Hook UserWillLogIn called but not implemented.") + return encodableError(fmt.Errorf("Hook UserWillLogIn called but not implemented.")) } return nil } @@ -487,7 +500,7 @@ func (g *hooksRPCClient) UserHasLoggedIn(c *Context, user *model.User) { g.log.Error("RPC call UserHasLoggedIn to plugin failed.", mlog.Err(err)) } } - return + } func (s *hooksRPCServer) UserHasLoggedIn(args *Z_UserHasLoggedInArgs, returns *Z_UserHasLoggedInReturns) error { @@ -495,8 +508,9 @@ func (s *hooksRPCServer) UserHasLoggedIn(args *Z_UserHasLoggedInArgs, returns *Z UserHasLoggedIn(c *Context, user *model.User) }); ok { hook.UserHasLoggedIn(args.A, args.B) + } else { - return fmt.Errorf("Hook UserHasLoggedIn called but not implemented.") + return encodableError(fmt.Errorf("Hook UserHasLoggedIn called but not implemented.")) } return nil } @@ -524,7 +538,7 @@ func (s *apiRPCServer) RegisterCommand(args *Z_RegisterCommandArgs, returns *Z_R }); ok { returns.A = hook.RegisterCommand(args.A) } else { - return fmt.Errorf("API RegisterCommand called but not implemented.") + return encodableError(fmt.Errorf("API RegisterCommand called but not implemented.")) } return nil } @@ -553,7 +567,7 @@ func (s *apiRPCServer) UnregisterCommand(args *Z_UnregisterCommandArgs, returns }); ok { returns.A = hook.UnregisterCommand(args.A, args.B) } else { - return fmt.Errorf("API UnregisterCommand called but not implemented.") + return encodableError(fmt.Errorf("API UnregisterCommand called but not implemented.")) } return nil } @@ -582,7 +596,7 @@ func (s *apiRPCServer) GetSession(args *Z_GetSessionArgs, returns *Z_GetSessionR }); ok { returns.A, returns.B = hook.GetSession(args.A) } else { - return fmt.Errorf("API GetSession called but not implemented.") + return encodableError(fmt.Errorf("API GetSession called but not implemented.")) } return nil } @@ -609,7 +623,7 @@ func (s *apiRPCServer) GetConfig(args *Z_GetConfigArgs, returns *Z_GetConfigRetu }); ok { returns.A = hook.GetConfig() } else { - return fmt.Errorf("API GetConfig called but not implemented.") + return encodableError(fmt.Errorf("API GetConfig called but not implemented.")) } return nil } @@ -637,7 +651,7 @@ func (s *apiRPCServer) SaveConfig(args *Z_SaveConfigArgs, returns *Z_SaveConfigR }); ok { returns.A = hook.SaveConfig(args.A) } else { - return fmt.Errorf("API SaveConfig called but not implemented.") + return encodableError(fmt.Errorf("API SaveConfig called but not implemented.")) } return nil } @@ -666,7 +680,7 @@ func (s *apiRPCServer) CreateUser(args *Z_CreateUserArgs, returns *Z_CreateUserR }); ok { returns.A, returns.B = hook.CreateUser(args.A) } else { - return fmt.Errorf("API CreateUser called but not implemented.") + return encodableError(fmt.Errorf("API CreateUser called but not implemented.")) } return nil } @@ -694,7 +708,7 @@ func (s *apiRPCServer) DeleteUser(args *Z_DeleteUserArgs, returns *Z_DeleteUserR }); ok { returns.A = hook.DeleteUser(args.A) } else { - return fmt.Errorf("API DeleteUser called but not implemented.") + return encodableError(fmt.Errorf("API DeleteUser called but not implemented.")) } return nil } @@ -723,7 +737,7 @@ func (s *apiRPCServer) GetUser(args *Z_GetUserArgs, returns *Z_GetUserReturns) e }); ok { returns.A, returns.B = hook.GetUser(args.A) } else { - return fmt.Errorf("API GetUser called but not implemented.") + return encodableError(fmt.Errorf("API GetUser called but not implemented.")) } return nil } @@ -752,7 +766,7 @@ func (s *apiRPCServer) GetUserByEmail(args *Z_GetUserByEmailArgs, returns *Z_Get }); ok { returns.A, returns.B = hook.GetUserByEmail(args.A) } else { - return fmt.Errorf("API GetUserByEmail called but not implemented.") + return encodableError(fmt.Errorf("API GetUserByEmail called but not implemented.")) } return nil } @@ -781,7 +795,7 @@ func (s *apiRPCServer) GetUserByUsername(args *Z_GetUserByUsernameArgs, returns }); ok { returns.A, returns.B = hook.GetUserByUsername(args.A) } else { - return fmt.Errorf("API GetUserByUsername called but not implemented.") + return encodableError(fmt.Errorf("API GetUserByUsername called but not implemented.")) } return nil } @@ -810,7 +824,7 @@ func (s *apiRPCServer) UpdateUser(args *Z_UpdateUserArgs, returns *Z_UpdateUserR }); ok { returns.A, returns.B = hook.UpdateUser(args.A) } else { - return fmt.Errorf("API UpdateUser called but not implemented.") + return encodableError(fmt.Errorf("API UpdateUser called but not implemented.")) } return nil } @@ -839,7 +853,7 @@ func (s *apiRPCServer) GetUserStatus(args *Z_GetUserStatusArgs, returns *Z_GetUs }); ok { returns.A, returns.B = hook.GetUserStatus(args.A) } else { - return fmt.Errorf("API GetUserStatus called but not implemented.") + return encodableError(fmt.Errorf("API GetUserStatus called but not implemented.")) } return nil } @@ -868,7 +882,7 @@ func (s *apiRPCServer) GetUserStatusesByIds(args *Z_GetUserStatusesByIdsArgs, re }); ok { returns.A, returns.B = hook.GetUserStatusesByIds(args.A) } else { - return fmt.Errorf("API GetUserStatusesByIds called but not implemented.") + return encodableError(fmt.Errorf("API GetUserStatusesByIds called but not implemented.")) } return nil } @@ -898,7 +912,7 @@ func (s *apiRPCServer) UpdateUserStatus(args *Z_UpdateUserStatusArgs, returns *Z }); ok { returns.A, returns.B = hook.UpdateUserStatus(args.A, args.B) } else { - return fmt.Errorf("API UpdateUserStatus called but not implemented.") + return encodableError(fmt.Errorf("API UpdateUserStatus called but not implemented.")) } return nil } @@ -928,7 +942,7 @@ func (s *apiRPCServer) GetLDAPUserAttributes(args *Z_GetLDAPUserAttributesArgs, }); ok { returns.A, returns.B = hook.GetLDAPUserAttributes(args.A, args.B) } else { - return fmt.Errorf("API GetLDAPUserAttributes called but not implemented.") + return encodableError(fmt.Errorf("API GetLDAPUserAttributes called but not implemented.")) } return nil } @@ -957,7 +971,7 @@ func (s *apiRPCServer) CreateTeam(args *Z_CreateTeamArgs, returns *Z_CreateTeamR }); ok { returns.A, returns.B = hook.CreateTeam(args.A) } else { - return fmt.Errorf("API CreateTeam called but not implemented.") + return encodableError(fmt.Errorf("API CreateTeam called but not implemented.")) } return nil } @@ -985,7 +999,7 @@ func (s *apiRPCServer) DeleteTeam(args *Z_DeleteTeamArgs, returns *Z_DeleteTeamR }); ok { returns.A = hook.DeleteTeam(args.A) } else { - return fmt.Errorf("API DeleteTeam called but not implemented.") + return encodableError(fmt.Errorf("API DeleteTeam called but not implemented.")) } return nil } @@ -1013,7 +1027,7 @@ func (s *apiRPCServer) GetTeams(args *Z_GetTeamsArgs, returns *Z_GetTeamsReturns }); ok { returns.A, returns.B = hook.GetTeams() } else { - return fmt.Errorf("API GetTeams called but not implemented.") + return encodableError(fmt.Errorf("API GetTeams called but not implemented.")) } return nil } @@ -1042,7 +1056,7 @@ func (s *apiRPCServer) GetTeam(args *Z_GetTeamArgs, returns *Z_GetTeamReturns) e }); ok { returns.A, returns.B = hook.GetTeam(args.A) } else { - return fmt.Errorf("API GetTeam called but not implemented.") + return encodableError(fmt.Errorf("API GetTeam called but not implemented.")) } return nil } @@ -1071,7 +1085,7 @@ func (s *apiRPCServer) GetTeamByName(args *Z_GetTeamByNameArgs, returns *Z_GetTe }); ok { returns.A, returns.B = hook.GetTeamByName(args.A) } else { - return fmt.Errorf("API GetTeamByName called but not implemented.") + return encodableError(fmt.Errorf("API GetTeamByName called but not implemented.")) } return nil } @@ -1100,7 +1114,7 @@ func (s *apiRPCServer) UpdateTeam(args *Z_UpdateTeamArgs, returns *Z_UpdateTeamR }); ok { returns.A, returns.B = hook.UpdateTeam(args.A) } else { - return fmt.Errorf("API UpdateTeam called but not implemented.") + return encodableError(fmt.Errorf("API UpdateTeam called but not implemented.")) } return nil } @@ -1130,7 +1144,7 @@ func (s *apiRPCServer) CreateTeamMember(args *Z_CreateTeamMemberArgs, returns *Z }); ok { returns.A, returns.B = hook.CreateTeamMember(args.A, args.B) } else { - return fmt.Errorf("API CreateTeamMember called but not implemented.") + return encodableError(fmt.Errorf("API CreateTeamMember called but not implemented.")) } return nil } @@ -1161,7 +1175,7 @@ func (s *apiRPCServer) CreateTeamMembers(args *Z_CreateTeamMembersArgs, returns }); ok { returns.A, returns.B = hook.CreateTeamMembers(args.A, args.B, args.C) } else { - return fmt.Errorf("API CreateTeamMembers called but not implemented.") + return encodableError(fmt.Errorf("API CreateTeamMembers called but not implemented.")) } return nil } @@ -1191,7 +1205,7 @@ func (s *apiRPCServer) DeleteTeamMember(args *Z_DeleteTeamMemberArgs, returns *Z }); ok { returns.A = hook.DeleteTeamMember(args.A, args.B, args.C) } else { - return fmt.Errorf("API DeleteTeamMember called but not implemented.") + return encodableError(fmt.Errorf("API DeleteTeamMember called but not implemented.")) } return nil } @@ -1222,7 +1236,7 @@ func (s *apiRPCServer) GetTeamMembers(args *Z_GetTeamMembersArgs, returns *Z_Get }); ok { returns.A, returns.B = hook.GetTeamMembers(args.A, args.B, args.C) } else { - return fmt.Errorf("API GetTeamMembers called but not implemented.") + return encodableError(fmt.Errorf("API GetTeamMembers called but not implemented.")) } return nil } @@ -1252,7 +1266,7 @@ func (s *apiRPCServer) GetTeamMember(args *Z_GetTeamMemberArgs, returns *Z_GetTe }); ok { returns.A, returns.B = hook.GetTeamMember(args.A, args.B) } else { - return fmt.Errorf("API GetTeamMember called but not implemented.") + return encodableError(fmt.Errorf("API GetTeamMember called but not implemented.")) } return nil } @@ -1283,7 +1297,7 @@ func (s *apiRPCServer) UpdateTeamMemberRoles(args *Z_UpdateTeamMemberRolesArgs, }); ok { returns.A, returns.B = hook.UpdateTeamMemberRoles(args.A, args.B, args.C) } else { - return fmt.Errorf("API UpdateTeamMemberRoles called but not implemented.") + return encodableError(fmt.Errorf("API UpdateTeamMemberRoles called but not implemented.")) } return nil } @@ -1312,7 +1326,7 @@ func (s *apiRPCServer) CreateChannel(args *Z_CreateChannelArgs, returns *Z_Creat }); ok { returns.A, returns.B = hook.CreateChannel(args.A) } else { - return fmt.Errorf("API CreateChannel called but not implemented.") + return encodableError(fmt.Errorf("API CreateChannel called but not implemented.")) } return nil } @@ -1340,7 +1354,7 @@ func (s *apiRPCServer) DeleteChannel(args *Z_DeleteChannelArgs, returns *Z_Delet }); ok { returns.A = hook.DeleteChannel(args.A) } else { - return fmt.Errorf("API DeleteChannel called but not implemented.") + return encodableError(fmt.Errorf("API DeleteChannel called but not implemented.")) } return nil } @@ -1371,7 +1385,7 @@ func (s *apiRPCServer) GetPublicChannelsForTeam(args *Z_GetPublicChannelsForTeam }); ok { returns.A, returns.B = hook.GetPublicChannelsForTeam(args.A, args.B, args.C) } else { - return fmt.Errorf("API GetPublicChannelsForTeam called but not implemented.") + return encodableError(fmt.Errorf("API GetPublicChannelsForTeam called but not implemented.")) } return nil } @@ -1400,7 +1414,7 @@ func (s *apiRPCServer) GetChannel(args *Z_GetChannelArgs, returns *Z_GetChannelR }); ok { returns.A, returns.B = hook.GetChannel(args.A) } else { - return fmt.Errorf("API GetChannel called but not implemented.") + return encodableError(fmt.Errorf("API GetChannel called but not implemented.")) } return nil } @@ -1431,7 +1445,7 @@ func (s *apiRPCServer) GetChannelByName(args *Z_GetChannelByNameArgs, returns *Z }); ok { returns.A, returns.B = hook.GetChannelByName(args.A, args.B, args.C) } else { - return fmt.Errorf("API GetChannelByName called but not implemented.") + return encodableError(fmt.Errorf("API GetChannelByName called but not implemented.")) } return nil } @@ -1462,7 +1476,7 @@ func (s *apiRPCServer) GetChannelByNameForTeamName(args *Z_GetChannelByNameForTe }); ok { returns.A, returns.B = hook.GetChannelByNameForTeamName(args.A, args.B, args.C) } else { - return fmt.Errorf("API GetChannelByNameForTeamName called but not implemented.") + return encodableError(fmt.Errorf("API GetChannelByNameForTeamName called but not implemented.")) } return nil } @@ -1492,7 +1506,7 @@ func (s *apiRPCServer) GetDirectChannel(args *Z_GetDirectChannelArgs, returns *Z }); ok { returns.A, returns.B = hook.GetDirectChannel(args.A, args.B) } else { - return fmt.Errorf("API GetDirectChannel called but not implemented.") + return encodableError(fmt.Errorf("API GetDirectChannel called but not implemented.")) } return nil } @@ -1521,7 +1535,7 @@ func (s *apiRPCServer) GetGroupChannel(args *Z_GetGroupChannelArgs, returns *Z_G }); ok { returns.A, returns.B = hook.GetGroupChannel(args.A) } else { - return fmt.Errorf("API GetGroupChannel called but not implemented.") + return encodableError(fmt.Errorf("API GetGroupChannel called but not implemented.")) } return nil } @@ -1550,7 +1564,7 @@ func (s *apiRPCServer) UpdateChannel(args *Z_UpdateChannelArgs, returns *Z_Updat }); ok { returns.A, returns.B = hook.UpdateChannel(args.A) } else { - return fmt.Errorf("API UpdateChannel called but not implemented.") + return encodableError(fmt.Errorf("API UpdateChannel called but not implemented.")) } return nil } @@ -1580,7 +1594,7 @@ func (s *apiRPCServer) AddChannelMember(args *Z_AddChannelMemberArgs, returns *Z }); ok { returns.A, returns.B = hook.AddChannelMember(args.A, args.B) } else { - return fmt.Errorf("API AddChannelMember called but not implemented.") + return encodableError(fmt.Errorf("API AddChannelMember called but not implemented.")) } return nil } @@ -1610,7 +1624,7 @@ func (s *apiRPCServer) GetChannelMember(args *Z_GetChannelMemberArgs, returns *Z }); ok { returns.A, returns.B = hook.GetChannelMember(args.A, args.B) } else { - return fmt.Errorf("API GetChannelMember called but not implemented.") + return encodableError(fmt.Errorf("API GetChannelMember called but not implemented.")) } return nil } @@ -1641,7 +1655,7 @@ func (s *apiRPCServer) UpdateChannelMemberRoles(args *Z_UpdateChannelMemberRoles }); ok { returns.A, returns.B = hook.UpdateChannelMemberRoles(args.A, args.B, args.C) } else { - return fmt.Errorf("API UpdateChannelMemberRoles called but not implemented.") + return encodableError(fmt.Errorf("API UpdateChannelMemberRoles called but not implemented.")) } return nil } @@ -1672,7 +1686,7 @@ func (s *apiRPCServer) UpdateChannelMemberNotifications(args *Z_UpdateChannelMem }); ok { returns.A, returns.B = hook.UpdateChannelMemberNotifications(args.A, args.B, args.C) } else { - return fmt.Errorf("API UpdateChannelMemberNotifications called but not implemented.") + return encodableError(fmt.Errorf("API UpdateChannelMemberNotifications called but not implemented.")) } return nil } @@ -1701,7 +1715,7 @@ func (s *apiRPCServer) DeleteChannelMember(args *Z_DeleteChannelMemberArgs, retu }); ok { returns.A = hook.DeleteChannelMember(args.A, args.B) } else { - return fmt.Errorf("API DeleteChannelMember called but not implemented.") + return encodableError(fmt.Errorf("API DeleteChannelMember called but not implemented.")) } return nil } @@ -1730,7 +1744,7 @@ func (s *apiRPCServer) CreatePost(args *Z_CreatePostArgs, returns *Z_CreatePostR }); ok { returns.A, returns.B = hook.CreatePost(args.A) } else { - return fmt.Errorf("API CreatePost called but not implemented.") + return encodableError(fmt.Errorf("API CreatePost called but not implemented.")) } return nil } @@ -1759,7 +1773,7 @@ func (s *apiRPCServer) AddReaction(args *Z_AddReactionArgs, returns *Z_AddReacti }); ok { returns.A, returns.B = hook.AddReaction(args.A) } else { - return fmt.Errorf("API AddReaction called but not implemented.") + return encodableError(fmt.Errorf("API AddReaction called but not implemented.")) } return nil } @@ -1787,7 +1801,7 @@ func (s *apiRPCServer) RemoveReaction(args *Z_RemoveReactionArgs, returns *Z_Rem }); ok { returns.A = hook.RemoveReaction(args.A) } else { - return fmt.Errorf("API RemoveReaction called but not implemented.") + return encodableError(fmt.Errorf("API RemoveReaction called but not implemented.")) } return nil } @@ -1816,7 +1830,7 @@ func (s *apiRPCServer) GetReactions(args *Z_GetReactionsArgs, returns *Z_GetReac }); ok { returns.A, returns.B = hook.GetReactions(args.A) } else { - return fmt.Errorf("API GetReactions called but not implemented.") + return encodableError(fmt.Errorf("API GetReactions called but not implemented.")) } return nil } @@ -1845,7 +1859,7 @@ func (s *apiRPCServer) SendEphemeralPost(args *Z_SendEphemeralPostArgs, returns }); ok { returns.A = hook.SendEphemeralPost(args.A, args.B) } else { - return fmt.Errorf("API SendEphemeralPost called but not implemented.") + return encodableError(fmt.Errorf("API SendEphemeralPost called but not implemented.")) } return nil } @@ -1873,7 +1887,7 @@ func (s *apiRPCServer) DeletePost(args *Z_DeletePostArgs, returns *Z_DeletePostR }); ok { returns.A = hook.DeletePost(args.A) } else { - return fmt.Errorf("API DeletePost called but not implemented.") + return encodableError(fmt.Errorf("API DeletePost called but not implemented.")) } return nil } @@ -1902,7 +1916,7 @@ func (s *apiRPCServer) GetPost(args *Z_GetPostArgs, returns *Z_GetPostReturns) e }); ok { returns.A, returns.B = hook.GetPost(args.A) } else { - return fmt.Errorf("API GetPost called but not implemented.") + return encodableError(fmt.Errorf("API GetPost called but not implemented.")) } return nil } @@ -1931,7 +1945,7 @@ func (s *apiRPCServer) UpdatePost(args *Z_UpdatePostArgs, returns *Z_UpdatePostR }); ok { returns.A, returns.B = hook.UpdatePost(args.A) } else { - return fmt.Errorf("API UpdatePost called but not implemented.") + return encodableError(fmt.Errorf("API UpdatePost called but not implemented.")) } return nil } @@ -1961,7 +1975,7 @@ func (s *apiRPCServer) CopyFileInfos(args *Z_CopyFileInfosArgs, returns *Z_CopyF }); ok { returns.A, returns.B = hook.CopyFileInfos(args.A, args.B) } else { - return fmt.Errorf("API CopyFileInfos called but not implemented.") + return encodableError(fmt.Errorf("API CopyFileInfos called but not implemented.")) } return nil } @@ -1990,7 +2004,7 @@ func (s *apiRPCServer) GetFileInfo(args *Z_GetFileInfoArgs, returns *Z_GetFileIn }); ok { returns.A, returns.B = hook.GetFileInfo(args.A) } else { - return fmt.Errorf("API GetFileInfo called but not implemented.") + return encodableError(fmt.Errorf("API GetFileInfo called but not implemented.")) } return nil } @@ -2019,7 +2033,7 @@ func (s *apiRPCServer) ReadFile(args *Z_ReadFileArgs, returns *Z_ReadFileReturns }); ok { returns.A, returns.B = hook.ReadFile(args.A) } else { - return fmt.Errorf("API ReadFile called but not implemented.") + return encodableError(fmt.Errorf("API ReadFile called but not implemented.")) } return nil } @@ -2048,7 +2062,7 @@ func (s *apiRPCServer) KVSet(args *Z_KVSetArgs, returns *Z_KVSetReturns) error { }); ok { returns.A = hook.KVSet(args.A, args.B) } else { - return fmt.Errorf("API KVSet called but not implemented.") + return encodableError(fmt.Errorf("API KVSet called but not implemented.")) } return nil } @@ -2077,7 +2091,7 @@ func (s *apiRPCServer) KVGet(args *Z_KVGetArgs, returns *Z_KVGetReturns) error { }); ok { returns.A, returns.B = hook.KVGet(args.A) } else { - return fmt.Errorf("API KVGet called but not implemented.") + return encodableError(fmt.Errorf("API KVGet called but not implemented.")) } return nil } @@ -2105,7 +2119,7 @@ func (s *apiRPCServer) KVDelete(args *Z_KVDeleteArgs, returns *Z_KVDeleteReturns }); ok { returns.A = hook.KVDelete(args.A) } else { - return fmt.Errorf("API KVDelete called but not implemented.") + return encodableError(fmt.Errorf("API KVDelete called but not implemented.")) } return nil } @@ -2125,7 +2139,7 @@ func (g *apiRPCClient) PublishWebSocketEvent(event string, payload map[string]in if err := g.client.Call("Plugin.PublishWebSocketEvent", _args, _returns); err != nil { log.Printf("RPC call to PublishWebSocketEvent API failed: %s", err.Error()) } - return + } func (s *apiRPCServer) PublishWebSocketEvent(args *Z_PublishWebSocketEventArgs, returns *Z_PublishWebSocketEventReturns) error { @@ -2134,7 +2148,7 @@ func (s *apiRPCServer) PublishWebSocketEvent(args *Z_PublishWebSocketEventArgs, }); ok { hook.PublishWebSocketEvent(args.A, args.B, args.C) } else { - return fmt.Errorf("API PublishWebSocketEvent called but not implemented.") + return encodableError(fmt.Errorf("API PublishWebSocketEvent called but not implemented.")) } return nil } @@ -2163,7 +2177,7 @@ func (s *apiRPCServer) HasPermissionTo(args *Z_HasPermissionToArgs, returns *Z_H }); ok { returns.A = hook.HasPermissionTo(args.A, args.B) } else { - return fmt.Errorf("API HasPermissionTo called but not implemented.") + return encodableError(fmt.Errorf("API HasPermissionTo called but not implemented.")) } return nil } @@ -2193,7 +2207,7 @@ func (s *apiRPCServer) HasPermissionToTeam(args *Z_HasPermissionToTeamArgs, retu }); ok { returns.A = hook.HasPermissionToTeam(args.A, args.B, args.C) } else { - return fmt.Errorf("API HasPermissionToTeam called but not implemented.") + return encodableError(fmt.Errorf("API HasPermissionToTeam called but not implemented.")) } return nil } @@ -2223,7 +2237,7 @@ func (s *apiRPCServer) HasPermissionToChannel(args *Z_HasPermissionToChannelArgs }); ok { returns.A = hook.HasPermissionToChannel(args.A, args.B, args.C) } else { - return fmt.Errorf("API HasPermissionToChannel called but not implemented.") + return encodableError(fmt.Errorf("API HasPermissionToChannel called but not implemented.")) } return nil } @@ -2242,7 +2256,7 @@ func (g *apiRPCClient) LogDebug(msg string, keyValuePairs ...interface{}) { if err := g.client.Call("Plugin.LogDebug", _args, _returns); err != nil { log.Printf("RPC call to LogDebug API failed: %s", err.Error()) } - return + } func (s *apiRPCServer) LogDebug(args *Z_LogDebugArgs, returns *Z_LogDebugReturns) error { @@ -2251,7 +2265,7 @@ func (s *apiRPCServer) LogDebug(args *Z_LogDebugArgs, returns *Z_LogDebugReturns }); ok { hook.LogDebug(args.A, args.B...) } else { - return fmt.Errorf("API LogDebug called but not implemented.") + return encodableError(fmt.Errorf("API LogDebug called but not implemented.")) } return nil } @@ -2270,7 +2284,7 @@ func (g *apiRPCClient) LogInfo(msg string, keyValuePairs ...interface{}) { if err := g.client.Call("Plugin.LogInfo", _args, _returns); err != nil { log.Printf("RPC call to LogInfo API failed: %s", err.Error()) } - return + } func (s *apiRPCServer) LogInfo(args *Z_LogInfoArgs, returns *Z_LogInfoReturns) error { @@ -2279,7 +2293,7 @@ func (s *apiRPCServer) LogInfo(args *Z_LogInfoArgs, returns *Z_LogInfoReturns) e }); ok { hook.LogInfo(args.A, args.B...) } else { - return fmt.Errorf("API LogInfo called but not implemented.") + return encodableError(fmt.Errorf("API LogInfo called but not implemented.")) } return nil } @@ -2298,7 +2312,7 @@ func (g *apiRPCClient) LogError(msg string, keyValuePairs ...interface{}) { if err := g.client.Call("Plugin.LogError", _args, _returns); err != nil { log.Printf("RPC call to LogError API failed: %s", err.Error()) } - return + } func (s *apiRPCServer) LogError(args *Z_LogErrorArgs, returns *Z_LogErrorReturns) error { @@ -2307,7 +2321,7 @@ func (s *apiRPCServer) LogError(args *Z_LogErrorArgs, returns *Z_LogErrorReturns }); ok { hook.LogError(args.A, args.B...) } else { - return fmt.Errorf("API LogError called but not implemented.") + return encodableError(fmt.Errorf("API LogError called but not implemented.")) } return nil } @@ -2326,7 +2340,7 @@ func (g *apiRPCClient) LogWarn(msg string, keyValuePairs ...interface{}) { if err := g.client.Call("Plugin.LogWarn", _args, _returns); err != nil { log.Printf("RPC call to LogWarn API failed: %s", err.Error()) } - return + } func (s *apiRPCServer) LogWarn(args *Z_LogWarnArgs, returns *Z_LogWarnReturns) error { @@ -2335,7 +2349,7 @@ func (s *apiRPCServer) LogWarn(args *Z_LogWarnArgs, returns *Z_LogWarnReturns) e }); ok { hook.LogWarn(args.A, args.B...) } else { - return fmt.Errorf("API LogWarn called but not implemented.") + return encodableError(fmt.Errorf("API LogWarn called but not implemented.")) } return nil } diff --git a/plugin/environment.go b/plugin/environment.go index 5c3a98349..55543e239 100644 --- a/plugin/environment.go +++ b/plugin/environment.go @@ -18,7 +18,6 @@ import ( ) type apiImplCreatorFunc func(*model.Manifest) API -type supervisorCreatorFunc func(*model.BundleInfo, *mlog.Logger, API) (*supervisor, error) // multiPluginHookRunnerFunc is a callback function to invoke as part of RunMultiPluginHook. // diff --git a/plugin/interface_generator/main.go b/plugin/interface_generator/main.go index b321c344a..95977713e 100644 --- a/plugin/interface_generator/main.go +++ b/plugin/interface_generator/main.go @@ -69,6 +69,43 @@ func FieldListToNames(fieldList *ast.FieldList, fileset *token.FileSet) string { return strings.Join(result, ", ") } +func FieldListToEncodedErrors(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string { + result := []string{} + if fieldList == nil { + return "" + } + + nextLetter := 'A' + for _, field := range fieldList.List { + typeNameBuffer := &bytes.Buffer{} + err := printer.Fprint(typeNameBuffer, fileset, field.Type) + if err != nil { + panic(err) + } + + if typeNameBuffer.String() != "error" { + nextLetter += 1 + continue + } + + name := "" + if len(field.Names) == 0 { + name = string(nextLetter) + nextLetter += 1 + } else { + for range field.Names { + name += string(nextLetter) + nextLetter += 1 + } + } + + result = append(result, structPrefix+name+" = encodableError("+structPrefix+name+")") + + } + + return strings.Join(result, "\n") +} + func FieldListDestruct(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string { result := []string{} if fieldList == nil || len(fieldList.List) == 0 { @@ -229,7 +266,7 @@ func (g *hooksRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} { g.log.Error("RPC call {{.Name}} to plugin failed.", mlog.Err(err)) } } - return {{destruct "_returns." .Return}} + {{ if .Return }} return {{destruct "_returns." .Return}} {{ end }} } func (s *hooksRPCServer) {{.Name}}(args *{{.Name | obscure}}Args, returns *{{.Name | obscure}}Returns) error { @@ -237,8 +274,9 @@ func (s *hooksRPCServer) {{.Name}}(args *{{.Name | obscure}}Args, returns *{{.Na {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} }); ok { {{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}}) + {{if .Return}}{{encodeErrors "returns." .Return}}{{end}} } else { - return fmt.Errorf("Hook {{.Name}} called but not implemented.") + return encodableError(fmt.Errorf("Hook {{.Name}} called but not implemented.")) } return nil } @@ -260,7 +298,7 @@ func (g *apiRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} { if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil { log.Printf("RPC call to {{.Name}} API failed: %s", err.Error()) } - return {{destruct "_returns." .Return}} + {{ if .Return }} return {{destruct "_returns." .Return}} {{ end }} } func (s *apiRPCServer) {{.Name}}(args *{{.Name | obscure}}Args, returns *{{.Name | obscure}}Returns) error { @@ -269,7 +307,7 @@ func (s *apiRPCServer) {{.Name}}(args *{{.Name | obscure}}Args, returns *{{.Name }); ok { {{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}}) } else { - return fmt.Errorf("API {{.Name}} called but not implemented.") + return encodableError(fmt.Errorf("API {{.Name}} called but not implemented.")) } return nil } @@ -292,6 +330,9 @@ func generateGlue(info *PluginInterfaceInfo) { "funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) }, "structStyle": func(fields *ast.FieldList) string { return FieldListToStructList(fields, info.FileSet) }, "valuesOnly": func(fields *ast.FieldList) string { return FieldListToNames(fields, info.FileSet) }, + "encodeErrors": func(structPrefix string, fields *ast.FieldList) string { + return FieldListToEncodedErrors(structPrefix, fields, info.FileSet) + }, "destruct": func(structPrefix string, fields *ast.FieldList) string { return FieldListDestruct(structPrefix, fields, info.FileSet) }, diff --git a/plugin/io_rpc.go b/plugin/io_rpc.go index 18a1eb525..fad7373a1 100644 --- a/plugin/io_rpc.go +++ b/plugin/io_rpc.go @@ -9,19 +9,6 @@ import ( "io" ) -type rwc struct { - io.ReadCloser - io.WriteCloser -} - -func (rwc *rwc) Close() (err error) { - err = rwc.WriteCloser.Close() - if rerr := rwc.ReadCloser.Close(); err == nil { - err = rerr - } - return -} - type remoteIOReader struct { conn io.ReadWriteCloser } diff --git a/plugin/supervisor.go b/plugin/supervisor.go index 33243e9cf..1165f5fb3 100644 --- a/plugin/supervisor.go +++ b/plugin/supervisor.go @@ -17,7 +17,6 @@ import ( ) type supervisor struct { - pluginId string client *plugin.Client hooks Hooks implemented [TotalHooksId]bool diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index fba37d7cb..4103980c5 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -301,6 +301,21 @@ func (s SqlChannelStore) CreateIndexesIfNotExists() { s.CreateFullTextIndexIfNotExists("idx_channel_search_txt", "Channels", "Name, DisplayName, Purpose") } +func (s SqlChannelStore) CreateTriggersIfNotExists() error { + // See SqlChannelStoreExperimental + return nil +} + +func (s SqlChannelStore) MigratePublicChannels() error { + // See SqlChannelStoreExperimental + return nil +} + +func (s SqlChannelStore) DropPublicChannels() error { + // See SqlChannelStoreExperimental + return nil +} + func (s SqlChannelStore) Save(channel *model.Channel, maxChannelsPerTeam int64) store.StoreChannel { return store.Do(func(result *store.StoreResult) { if channel.DeleteAt != 0 { @@ -804,12 +819,12 @@ func (s SqlChannelStore) GetTeamChannels(teamId string) store.StoreChannel { _, err := s.GetReplica().Select(data, "SELECT * FROM Channels WHERE TeamId = :TeamId And Type != 'D' ORDER BY DisplayName", map[string]interface{}{"TeamId": teamId}) if err != nil { - result.Err = model.NewAppError("SqlChannelStore.GetChannels", "store.sql_channel.get_channels.get.app_error", nil, "teamId="+teamId+", err="+err.Error(), http.StatusInternalServerError) + result.Err = model.NewAppError("SqlChannelStore.GetTeamChannels", "store.sql_channel.get_channels.get.app_error", nil, "teamId="+teamId+", err="+err.Error(), http.StatusInternalServerError) return } if len(*data) == 0 { - result.Err = model.NewAppError("SqlChannelStore.GetChannels", "store.sql_channel.get_channels.not_found.app_error", nil, "teamId="+teamId, http.StatusNotFound) + result.Err = model.NewAppError("SqlChannelStore.GetTeamChannels", "store.sql_channel.get_channels.not_found.app_error", nil, "teamId="+teamId, http.StatusNotFound) return } @@ -962,16 +977,16 @@ var CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY = ` TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole, ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole, ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole - FROM + FROM ChannelMembers - INNER JOIN + INNER JOIN Channels ON ChannelMembers.ChannelId = Channels.Id LEFT JOIN Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id LEFT JOIN Teams ON Channels.TeamId = Teams.Id LEFT JOIN - Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id + Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id ` func (s SqlChannelStore) SaveMember(member *model.ChannelMember) store.StoreChannel { @@ -1589,6 +1604,53 @@ func (s SqlChannelStore) AutocompleteInTeam(teamId string, term string, includeD }) } +func (s SqlChannelStore) AutocompleteInTeamForSearch(teamId string, userId string, term string, includeDeleted bool) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + deleteFilter := "AND DeleteAt = 0" + if includeDeleted { + deleteFilter = "" + } + + queryFormat := ` + SELECT + C.* + FROM + Channels AS C + JOIN + ChannelMembers AS CM ON CM.ChannelId = C.Id + WHERE + C.TeamId = :TeamId + AND CM.UserId = :UserId + ` + deleteFilter + ` + %v + LIMIT 50` + + var channels model.ChannelList + + if likeClause, likeTerm := s.buildLIKEClause(term); likeClause == "" { + if _, err := s.GetReplica().Select(&channels, fmt.Sprintf(queryFormat, ""), map[string]interface{}{"TeamId": teamId, "UserId": userId}); err != nil { + result.Err = model.NewAppError("SqlChannelStore.AutocompleteInTeamForSearch", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError) + } + } else { + // Using a UNION results in index_merge and fulltext queries and is much faster than the ref + // query you would get using an OR of the LIKE and full-text clauses. + fulltextClause, fulltextTerm := s.buildFulltextClause(term) + likeQuery := fmt.Sprintf(queryFormat, "AND "+likeClause) + fulltextQuery := fmt.Sprintf(queryFormat, "AND "+fulltextClause) + query := fmt.Sprintf("(%v) UNION (%v) LIMIT 50", likeQuery, fulltextQuery) + + if _, err := s.GetReplica().Select(&channels, query, map[string]interface{}{"TeamId": teamId, "UserId": userId, "LikeTerm": likeTerm, "FulltextTerm": fulltextTerm}); err != nil { + result.Err = model.NewAppError("SqlChannelStore.AutocompleteInTeamForSearch", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError) + } + } + + sort.Slice(channels, func(a, b int) bool { + return strings.ToLower(channels[a].DisplayName) < strings.ToLower(channels[b].DisplayName) + }) + result.Data = &channels + }) +} + func (s SqlChannelStore) SearchInTeam(teamId string, term string, includeDeleted bool) store.StoreChannel { return store.Do(func(result *store.StoreResult) { deleteFilter := "AND DeleteAt = 0" @@ -1864,7 +1926,7 @@ func (s SqlChannelStore) ClearAllCustomRoleAssignments() store.StoreChannel { lastUserId := strings.Repeat("0", 26) lastChannelId := strings.Repeat("0", 26) - for true { + for { var transaction *gorp.Transaction var err error @@ -1941,3 +2003,16 @@ func (s SqlChannelStore) ResetLastPostAt() store.StoreChannel { } }) } + +func (s SqlChannelStore) EnableExperimentalPublicChannelsMaterialization() { + // See SqlChannelStoreExperimental +} + +func (s SqlChannelStore) DisableExperimentalPublicChannelsMaterialization() { + // See SqlChannelStoreExperimental +} + +func (s SqlChannelStore) IsExperimentalPublicChannelsMaterializationEnabled() bool { + // See SqlChannelStoreExperimental + return false +} diff --git a/store/sqlstore/channel_store_experimental.go b/store/sqlstore/channel_store_experimental.go new file mode 100644 index 000000000..67576ddc1 --- /dev/null +++ b/store/sqlstore/channel_store_experimental.go @@ -0,0 +1,819 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package sqlstore + +import ( + "fmt" + "net/http" + "sort" + "strconv" + "strings" + "sync/atomic" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost-server/einterfaces" + "github.com/mattermost/mattermost-server/mlog" + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/store" +) + +// publicChannel is a subset of the metadata corresponding to public channels only. +type publicChannel struct { + Id string `json:"id"` + DeleteAt int64 `json:"delete_at"` + TeamId string `json:"team_id"` + DisplayName string `json:"display_name"` + Name string `json:"name"` + Header string `json:"header"` + Purpose string `json:"purpose"` +} + +type SqlChannelStoreExperimental struct { + SqlChannelStore + experimentalPublicChannelsMaterializationDisabled *uint32 +} + +func NewSqlChannelStoreExperimental(sqlStore SqlStore, metrics einterfaces.MetricsInterface, enabled bool) store.ChannelStore { + s := &SqlChannelStoreExperimental{ + SqlChannelStore: *NewSqlChannelStore(sqlStore, metrics).(*SqlChannelStore), + experimentalPublicChannelsMaterializationDisabled: new(uint32), + } + + if enabled { + // Forcibly log, since the default state is enabled and we want this on startup. + mlog.Info("Enabling experimental public channels materialization") + s.EnableExperimentalPublicChannelsMaterialization() + } else { + s.DisableExperimentalPublicChannelsMaterialization() + } + + if s.IsExperimentalPublicChannelsMaterializationEnabled() { + for _, db := range sqlStore.GetAllConns() { + tablePublicChannels := db.AddTableWithName(publicChannel{}, "PublicChannels").SetKeys(false, "Id") + tablePublicChannels.ColMap("Id").SetMaxSize(26) + tablePublicChannels.ColMap("TeamId").SetMaxSize(26) + tablePublicChannels.ColMap("DisplayName").SetMaxSize(64) + tablePublicChannels.ColMap("Name").SetMaxSize(64) + tablePublicChannels.SetUniqueTogether("Name", "TeamId") + tablePublicChannels.ColMap("Header").SetMaxSize(1024) + tablePublicChannels.ColMap("Purpose").SetMaxSize(250) + } + } + + return s +} + +// migratePublicChannels initializes the PublicChannels table with data created before the triggers +// took over keeping it up-to-date. +func (s SqlChannelStoreExperimental) MigratePublicChannels() error { + if !s.IsExperimentalPublicChannelsMaterializationEnabled() { + return s.SqlChannelStore.MigratePublicChannels() + } + + transaction, err := s.GetMaster().Begin() + if err != nil { + return err + } + + if _, err := transaction.Exec(` + INSERT INTO PublicChannels + (Id, DeleteAt, TeamId, DisplayName, Name, Header, Purpose) + SELECT + c.Id, c.DeleteAt, c.TeamId, c.DisplayName, c.Name, c.Header, c.Purpose + FROM + Channels c + LEFT JOIN + PublicChannels pc ON (pc.Id = c.Id) + WHERE + c.Type = 'O' + AND pc.Id IS NULL + `); err != nil { + return err + } + + if err := transaction.Commit(); err != nil { + return err + } + + return nil +} + +// DropPublicChannels removes the public channels table and all associated triggers. +func (s SqlChannelStoreExperimental) DropPublicChannels() error { + // Only PostgreSQL will honour the transaction when executing the DDL changes below. + transaction, err := s.GetMaster().Begin() + if err != nil { + return err + } + + if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { + if _, err := transaction.Exec(` + DROP TRIGGER IF EXISTS trigger_channels ON Channels + `); err != nil { + return err + } + if _, err := transaction.Exec(` + DROP FUNCTION IF EXISTS channels_copy_to_public_channels + `); err != nil { + return err + } + } else if s.DriverName() == model.DATABASE_DRIVER_MYSQL { + if _, err := transaction.Exec(` + DROP TRIGGER IF EXISTS trigger_channels_insert + `); err != nil { + return err + } + if _, err := transaction.Exec(` + DROP TRIGGER IF EXISTS trigger_channels_update + `); err != nil { + return err + } + if _, err := transaction.Exec(` + DROP TRIGGER IF EXISTS trigger_channels_delete + `); err != nil { + return err + } + } else if s.DriverName() == model.DATABASE_DRIVER_SQLITE { + if _, err := transaction.Exec(` + DROP TRIGGER IF EXISTS trigger_channels_insert + `); err != nil { + return err + } + if _, err := transaction.Exec(` + DROP TRIGGER IF EXISTS trigger_channels_update_delete + `); err != nil { + return err + } + if _, err := transaction.Exec(` + DROP TRIGGER IF EXISTS trigger_channels_update + `); err != nil { + return err + } + if _, err := transaction.Exec(` + DROP TRIGGER IF EXISTS trigger_channels_delete + `); err != nil { + return err + } + } else { + return errors.New("failed to create trigger because of missing driver") + } + + if _, err := transaction.Exec(` + DROP TABLE IF EXISTS PublicChannels + `); err != nil { + return err + } + + if err := transaction.Commit(); err != nil { + return err + } + + return nil +} + +func (s SqlChannelStoreExperimental) CreateIndexesIfNotExists() { + s.SqlChannelStore.CreateIndexesIfNotExists() + + if !s.IsExperimentalPublicChannelsMaterializationEnabled() { + return + } + + s.CreateIndexIfNotExists("idx_publicchannels_team_id", "PublicChannels", "TeamId") + s.CreateIndexIfNotExists("idx_publicchannels_name", "PublicChannels", "Name") + s.CreateIndexIfNotExists("idx_publicchannels_delete_at", "PublicChannels", "DeleteAt") + if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { + s.CreateIndexIfNotExists("idx_publicchannels_name_lower", "PublicChannels", "lower(Name)") + s.CreateIndexIfNotExists("idx_publicchannels_displayname_lower", "PublicChannels", "lower(DisplayName)") + } + s.CreateFullTextIndexIfNotExists("idx_publicchannels_search_txt", "PublicChannels", "Name, DisplayName, Purpose") +} + +func (s SqlChannelStoreExperimental) CreateTriggersIfNotExists() error { + s.SqlChannelStore.CreateTriggersIfNotExists() + + if !s.IsExperimentalPublicChannelsMaterializationEnabled() { + return nil + } + + if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { + if !s.DoesTriggerExist("trigger_channels") { + transaction, err := s.GetMaster().Begin() + if err != nil { + return errors.Wrap(err, "failed to create trigger function") + } + + if _, err := transaction.ExecNoTimeout(` + CREATE OR REPLACE FUNCTION channels_copy_to_public_channels() RETURNS TRIGGER + SECURITY DEFINER + LANGUAGE plpgsql + AS $$ + DECLARE + counter int := 0; + BEGIN + IF (TG_OP = 'DELETE' AND OLD.Type = 'O') OR (TG_OP = 'UPDATE' AND NEW.Type != 'O') THEN + DELETE FROM + PublicChannels + WHERE + Id = OLD.Id; + ELSEIF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') AND NEW.Type = 'O' THEN + UPDATE + PublicChannels + SET + DeleteAt = NEW.DeleteAt, + TeamId = NEW.TeamId, + DisplayName = NEW.DisplayName, + Name = NEW.Name, + Header = NEW.Header, + Purpose = NEW.Purpose + WHERE + Id = NEW.Id; + + -- There's a race condition here where the INSERT might fail, though this should only occur + -- if PublicChannels had been modified outside of the triggers. We could improve this with + -- the UPSERT functionality in Postgres 9.5+ once we support same. + IF NOT FOUND THEN + INSERT INTO + PublicChannels(Id, DeleteAt, TeamId, DisplayName, Name, Header, Purpose) + VALUES + (NEW.Id, NEW.DeleteAt, NEW.TeamId, NEW.DisplayName, NEW.Name, NEW.Header, NEW.Purpose); + END IF; + END IF; + + RETURN NULL; + END + $$; + `); err != nil { + return errors.Wrap(err, "failed to create trigger function") + } + + if _, err := transaction.ExecNoTimeout(` + CREATE TRIGGER + trigger_channels + AFTER INSERT OR UPDATE OR DELETE ON + Channels + FOR EACH ROW EXECUTE PROCEDURE + channels_copy_to_public_channels(); + `); err != nil { + return errors.Wrap(err, "failed to create trigger") + } + + if err := transaction.Commit(); err != nil { + return errors.Wrap(err, "failed to create trigger function") + } + } + } else if s.DriverName() == model.DATABASE_DRIVER_MYSQL { + // Note that DDL statements in MySQL (CREATE TABLE, CREATE TRIGGER, etc.) cannot + // be rolled back inside a transaction (unlike PostgreSQL), so there's no point in + // wrapping what follows inside a transaction. + + if !s.DoesTriggerExist("trigger_channels_insert") { + if _, err := s.GetMaster().ExecNoTimeout(` + CREATE TRIGGER + trigger_channels_insert + AFTER INSERT ON + Channels + FOR EACH ROW + BEGIN + IF NEW.Type = 'O' THEN + INSERT INTO + PublicChannels(Id, DeleteAt, TeamId, DisplayName, Name, Header, Purpose) + VALUES + (NEW.Id, NEW.DeleteAt, NEW.TeamId, NEW.DisplayName, NEW.Name, NEW.Header, NEW.Purpose) + ON DUPLICATE KEY UPDATE + DeleteAt = NEW.DeleteAt, + TeamId = NEW.TeamId, + DisplayName = NEW.DisplayName, + Name = NEW.Name, + Header = NEW.Header, + Purpose = NEW.Purpose; + END IF; + END; + `); err != nil { + return errors.Wrap(err, "failed to create trigger_channels_insert trigger") + } + } + + if !s.DoesTriggerExist("trigger_channels_update") { + if _, err := s.GetMaster().ExecNoTimeout(` + CREATE TRIGGER + trigger_channels_update + AFTER UPDATE ON + Channels + FOR EACH ROW + BEGIN + IF OLD.Type = 'O' AND NEW.Type != 'O' THEN + DELETE FROM + PublicChannels + WHERE + Id = NEW.Id; + ELSEIF NEW.Type = 'O' THEN + INSERT INTO + PublicChannels(Id, DeleteAt, TeamId, DisplayName, Name, Header, Purpose) + VALUES + (NEW.Id, NEW.DeleteAt, NEW.TeamId, NEW.DisplayName, NEW.Name, NEW.Header, NEW.Purpose) + ON DUPLICATE KEY UPDATE + DeleteAt = NEW.DeleteAt, + TeamId = NEW.TeamId, + DisplayName = NEW.DisplayName, + Name = NEW.Name, + Header = NEW.Header, + Purpose = NEW.Purpose; + END IF; + END; + `); err != nil { + return errors.Wrap(err, "failed to create trigger_channels_update trigger") + } + } + + if !s.DoesTriggerExist("trigger_channels_delete") { + if _, err := s.GetMaster().ExecNoTimeout(` + CREATE TRIGGER + trigger_channels_delete + AFTER DELETE ON + Channels + FOR EACH ROW + BEGIN + IF OLD.Type = 'O' THEN + DELETE FROM + PublicChannels + WHERE + Id = OLD.Id; + END IF; + END; + `); err != nil { + return errors.Wrap(err, "failed to create trigger_channels_delete trigger") + } + } + } else if s.DriverName() == model.DATABASE_DRIVER_SQLITE { + if _, err := s.GetMaster().ExecNoTimeout(` + CREATE TRIGGER IF NOT EXISTS + trigger_channels_insert + AFTER INSERT ON + Channels + FOR EACH ROW + WHEN NEW.Type = 'O' + BEGIN + -- Ideally, we'd leverage ON CONFLICT DO UPDATE below and make this INSERT resilient to pre-existing + -- data. However, the version of Sqlite we're compiling against doesn't support this. This isn't + -- critical, though, since we don't support Sqlite in production. + INSERT INTO + PublicChannels(Id, DeleteAt, TeamId, DisplayName, Name, Header, Purpose) + VALUES + (NEW.Id, NEW.DeleteAt, NEW.TeamId, NEW.DisplayName, NEW.Name, NEW.Header, NEW.Purpose); + END; + `); err != nil { + return errors.Wrap(err, "failed to create trigger_channels_insert trigger") + } + + if _, err := s.GetMaster().ExecNoTimeout(` + CREATE TRIGGER IF NOT EXISTS + trigger_channels_update_delete + AFTER UPDATE ON + Channels + FOR EACH ROW + WHEN + OLD.Type = 'O' + AND NEW.Type != 'O' + BEGIN + DELETE FROM + PublicChannels + WHERE + Id = NEW.Id; + END; + `); err != nil { + return errors.Wrap(err, "failed to create trigger_channels_update_delete trigger") + } + + if _, err := s.GetMaster().ExecNoTimeout(` + CREATE TRIGGER IF NOT EXISTS + trigger_channels_update + AFTER UPDATE ON + Channels + FOR EACH ROW + WHEN + OLD.Type != 'O' + AND NEW.Type = 'O' + BEGIN + -- See comments re: ON CONFLICT DO UPDATE above that would apply here as well. + UPDATE + PublicChannels + SET + DeleteAt = NEW.DeleteAt, + TeamId = NEW.TeamId, + DisplayName = NEW.DisplayName, + Name = NEW.Name, + Header = NEW.Header, + Purpose = NEW.Purpose + WHERE + Id = NEW.Id; + END; + `); err != nil { + return errors.Wrap(err, "failed to create trigger_channels_update trigger") + } + + if _, err := s.GetMaster().ExecNoTimeout(` + CREATE TRIGGER IF NOT EXISTS + trigger_channels_delete + AFTER UPDATE ON + Channels + FOR EACH ROW + WHEN + OLD.Type = 'O' + BEGIN + DELETE FROM + PublicChannels + WHERE + Id = OLD.Id; + END; + `); err != nil { + return errors.Wrap(err, "failed to create trigger_channels_delete trigger") + } + } else { + return errors.New("failed to create trigger because of missing driver") + } + + return nil +} + +func (s SqlChannelStoreExperimental) GetMoreChannels(teamId string, userId string, offset int, limit int) store.StoreChannel { + if !s.IsExperimentalPublicChannelsMaterializationEnabled() { + return s.SqlChannelStore.GetMoreChannels(teamId, userId, offset, limit) + } + + return store.Do(func(result *store.StoreResult) { + data := &model.ChannelList{} + _, err := s.GetReplica().Select(data, ` + SELECT + Channels.* + FROM + Channels + JOIN + PublicChannels c ON (c.Id = Channels.Id) + WHERE + c.TeamId = :TeamId + AND c.DeleteAt = 0 + AND c.Id NOT IN ( + SELECT + c.Id + FROM + PublicChannels c + JOIN + ChannelMembers cm ON (cm.ChannelId = c.Id) + WHERE + c.TeamId = :TeamId + AND cm.UserId = :UserId + AND c.DeleteAt = 0 + ) + ORDER BY + c.DisplayName + LIMIT :Limit + OFFSET :Offset + `, map[string]interface{}{ + "TeamId": teamId, + "UserId": userId, + "Limit": limit, + "Offset": offset, + }) + + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.GetMoreChannels", "store.sql_channel.get_more_channels.get.app_error", nil, "teamId="+teamId+", userId="+userId+", err="+err.Error(), http.StatusInternalServerError) + return + } + + result.Data = data + }) +} + +func (s SqlChannelStoreExperimental) GetPublicChannelsForTeam(teamId string, offset int, limit int) store.StoreChannel { + if !s.IsExperimentalPublicChannelsMaterializationEnabled() { + return s.SqlChannelStore.GetPublicChannelsForTeam(teamId, offset, limit) + } + + return store.Do(func(result *store.StoreResult) { + data := &model.ChannelList{} + _, err := s.GetReplica().Select(data, ` + SELECT + Channels.* + FROM + Channels + JOIN + PublicChannels pc ON (pc.Id = Channels.Id) + WHERE + pc.TeamId = :TeamId + AND pc.DeleteAt = 0 + ORDER BY pc.DisplayName + LIMIT :Limit + OFFSET :Offset + `, map[string]interface{}{ + "TeamId": teamId, + "Limit": limit, + "Offset": offset, + }) + + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.GetPublicChannelsForTeam", "store.sql_channel.get_public_channels.get.app_error", nil, "teamId="+teamId+", err="+err.Error(), http.StatusInternalServerError) + return + } + + result.Data = data + }) +} + +func (s SqlChannelStoreExperimental) GetPublicChannelsByIdsForTeam(teamId string, channelIds []string) store.StoreChannel { + if !s.IsExperimentalPublicChannelsMaterializationEnabled() { + return s.SqlChannelStore.GetPublicChannelsByIdsForTeam(teamId, channelIds) + } + + return store.Do(func(result *store.StoreResult) { + props := make(map[string]interface{}) + props["teamId"] = teamId + + idQuery := "" + + for index, channelId := range channelIds { + if len(idQuery) > 0 { + idQuery += ", " + } + + props["channelId"+strconv.Itoa(index)] = channelId + idQuery += ":channelId" + strconv.Itoa(index) + } + + data := &model.ChannelList{} + _, err := s.GetReplica().Select(data, ` + SELECT + Channels.* + FROM + Channels + JOIN + PublicChannels pc ON (pc.Id = Channels.Id) + WHERE + pc.TeamId = :teamId + AND pc.DeleteAt = 0 + AND pc.Id IN (`+idQuery+`) + ORDER BY pc.DisplayName + `, props) + + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.GetPublicChannelsByIdsForTeam", "store.sql_channel.get_channels_by_ids.get.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + if len(*data) == 0 { + result.Err = model.NewAppError("SqlChannelStore.GetPublicChannelsByIdsForTeam", "store.sql_channel.get_channels_by_ids.not_found.app_error", nil, "", http.StatusNotFound) + } + + result.Data = data + }) +} + +func (s SqlChannelStoreExperimental) AutocompleteInTeam(teamId string, term string, includeDeleted bool) store.StoreChannel { + if !s.IsExperimentalPublicChannelsMaterializationEnabled() { + return s.SqlChannelStore.AutocompleteInTeam(teamId, term, includeDeleted) + } + + return store.Do(func(result *store.StoreResult) { + deleteFilter := "AND c.DeleteAt = 0" + if includeDeleted { + deleteFilter = "" + } + + queryFormat := ` + SELECT + Channels.* + FROM + Channels + JOIN + PublicChannels c ON (c.Id = Channels.Id) + WHERE + c.TeamId = :TeamId + ` + deleteFilter + ` + %v + LIMIT 50 + ` + + var channels model.ChannelList + + if likeClause, likeTerm := s.buildLIKEClause(term); likeClause == "" { + if _, err := s.GetReplica().Select(&channels, fmt.Sprintf(queryFormat, ""), map[string]interface{}{"TeamId": teamId}); err != nil { + result.Err = model.NewAppError("SqlChannelStore.AutocompleteInTeam", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError) + } + } else { + // Using a UNION results in index_merge and fulltext queries and is much faster than the ref + // query you would get using an OR of the LIKE and full-text clauses. + fulltextClause, fulltextTerm := s.buildFulltextClause(term) + likeQuery := fmt.Sprintf(queryFormat, "AND "+likeClause) + fulltextQuery := fmt.Sprintf(queryFormat, "AND "+fulltextClause) + query := fmt.Sprintf("(%v) UNION (%v) LIMIT 50", likeQuery, fulltextQuery) + + if _, err := s.GetReplica().Select(&channels, query, map[string]interface{}{"TeamId": teamId, "LikeTerm": likeTerm, "FulltextTerm": fulltextTerm}); err != nil { + result.Err = model.NewAppError("SqlChannelStore.AutocompleteInTeam", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError) + } + } + + sort.Slice(channels, func(a, b int) bool { + return strings.ToLower(channels[a].DisplayName) < strings.ToLower(channels[b].DisplayName) + }) + result.Data = &channels + }) +} + +func (s SqlChannelStoreExperimental) SearchInTeam(teamId string, term string, includeDeleted bool) store.StoreChannel { + if !s.IsExperimentalPublicChannelsMaterializationEnabled() { + return s.SqlChannelStore.SearchInTeam(teamId, term, includeDeleted) + } + + return store.Do(func(result *store.StoreResult) { + deleteFilter := "AND c.DeleteAt = 0" + if includeDeleted { + deleteFilter = "" + } + + *result = s.performSearch(` + SELECT + Channels.* + FROM + Channels + JOIN + PublicChannels c ON (c.Id = Channels.Id) + WHERE + c.TeamId = :TeamId + `+deleteFilter+` + SEARCH_CLAUSE + ORDER BY c.DisplayName + LIMIT 100 + `, term, map[string]interface{}{ + "TeamId": teamId, + }) + }) +} + +func (s SqlChannelStoreExperimental) SearchMore(userId string, teamId string, term string) store.StoreChannel { + if !s.IsExperimentalPublicChannelsMaterializationEnabled() { + return s.SqlChannelStore.SearchMore(userId, teamId, term) + } + + return store.Do(func(result *store.StoreResult) { + *result = s.performSearch(` + SELECT + Channels.* + FROM + Channels + JOIN + PublicChannels c ON (c.Id = Channels.Id) + WHERE + c.TeamId = :TeamId + AND c.DeleteAt = 0 + AND c.Id NOT IN ( + SELECT + c.Id + FROM + PublicChannels c + JOIN + ChannelMembers cm ON (cm.ChannelId = c.Id) + WHERE + c.TeamId = :TeamId + AND cm.UserId = :UserId + AND c.DeleteAt = 0 + ) + SEARCH_CLAUSE + ORDER BY c.DisplayName + LIMIT 100 + `, term, map[string]interface{}{ + "TeamId": teamId, + "UserId": userId, + }) + }) +} + +func (s SqlChannelStoreExperimental) buildLIKEClause(term string) (likeClause, likeTerm string) { + if !s.IsExperimentalPublicChannelsMaterializationEnabled() { + return s.SqlChannelStore.buildLIKEClause(term) + } + + likeTerm = term + searchColumns := "c.Name, c.DisplayName, c.Purpose" + + // These chars must be removed from the like query. + for _, c := range ignoreLikeSearchChar { + likeTerm = strings.Replace(likeTerm, c, "", -1) + } + + // These chars must be escaped in the like query. + for _, c := range escapeLikeSearchChar { + likeTerm = strings.Replace(likeTerm, c, "*"+c, -1) + } + + if likeTerm == "" { + return + } + + // Prepare the LIKE portion of the query. + var searchFields []string + for _, field := range strings.Split(searchColumns, ", ") { + if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { + searchFields = append(searchFields, fmt.Sprintf("lower(%s) LIKE lower(%s) escape '*'", field, ":LikeTerm")) + } else { + searchFields = append(searchFields, fmt.Sprintf("%s LIKE %s escape '*'", field, ":LikeTerm")) + } + } + + likeClause = fmt.Sprintf("(%s)", strings.Join(searchFields, " OR ")) + likeTerm += "%" + return +} + +func (s SqlChannelStoreExperimental) buildFulltextClause(term string) (fulltextClause, fulltextTerm string) { + if !s.IsExperimentalPublicChannelsMaterializationEnabled() { + return s.SqlChannelStore.buildFulltextClause(term) + } + + // Copy the terms as we will need to prepare them differently for each search type. + fulltextTerm = term + + searchColumns := "c.Name, c.DisplayName, c.Purpose" + + // These chars must be treated as spaces in the fulltext query. + for _, c := range spaceFulltextSearchChar { + fulltextTerm = strings.Replace(fulltextTerm, c, " ", -1) + } + + // Prepare the FULLTEXT portion of the query. + if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { + fulltextTerm = strings.Replace(fulltextTerm, "|", "", -1) + + splitTerm := strings.Fields(fulltextTerm) + for i, t := range strings.Fields(fulltextTerm) { + if i == len(splitTerm)-1 { + splitTerm[i] = t + ":*" + } else { + splitTerm[i] = t + ":* &" + } + } + + fulltextTerm = strings.Join(splitTerm, " ") + + fulltextClause = fmt.Sprintf("((%s) @@ to_tsquery(:FulltextTerm))", convertMySQLFullTextColumnsToPostgres(searchColumns)) + } else if s.DriverName() == model.DATABASE_DRIVER_MYSQL { + splitTerm := strings.Fields(fulltextTerm) + for i, t := range strings.Fields(fulltextTerm) { + splitTerm[i] = "+" + t + "*" + } + + fulltextTerm = strings.Join(splitTerm, " ") + + fulltextClause = fmt.Sprintf("MATCH(%s) AGAINST (:FulltextTerm IN BOOLEAN MODE)", searchColumns) + } + + return +} + +func (s SqlChannelStoreExperimental) performSearch(searchQuery string, term string, parameters map[string]interface{}) store.StoreResult { + if !s.IsExperimentalPublicChannelsMaterializationEnabled() { + return s.SqlChannelStore.performSearch(searchQuery, term, parameters) + } + + result := store.StoreResult{} + + likeClause, likeTerm := s.buildLIKEClause(term) + if likeTerm == "" { + // If the likeTerm is empty after preparing, then don't bother searching. + searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1) + } else { + parameters["LikeTerm"] = likeTerm + fulltextClause, fulltextTerm := s.buildFulltextClause(term) + parameters["FulltextTerm"] = fulltextTerm + searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "AND ("+likeClause+" OR "+fulltextClause+")", 1) + } + + var channels model.ChannelList + + if _, err := s.GetReplica().Select(&channels, searchQuery, parameters); err != nil { + result.Err = model.NewAppError("SqlChannelStore.Search", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError) + return result + } + + result.Data = &channels + return result +} + +func (s SqlChannelStoreExperimental) EnableExperimentalPublicChannelsMaterialization() { + if !s.IsExperimentalPublicChannelsMaterializationEnabled() { + mlog.Info("Enabling experimental public channels materialization") + } + + atomic.StoreUint32(s.experimentalPublicChannelsMaterializationDisabled, 0) +} + +func (s SqlChannelStoreExperimental) DisableExperimentalPublicChannelsMaterialization() { + if s.IsExperimentalPublicChannelsMaterializationEnabled() { + mlog.Info("Disabling experimental public channels materialization") + } + + atomic.StoreUint32(s.experimentalPublicChannelsMaterializationDisabled, 1) +} + +func (s SqlChannelStoreExperimental) IsExperimentalPublicChannelsMaterializationEnabled() bool { + return atomic.LoadUint32(s.experimentalPublicChannelsMaterializationDisabled) == 0 +} diff --git a/store/sqlstore/channel_store_test.go b/store/sqlstore/channel_store_test.go index 0e8b4191a..5eb84afcd 100644 --- a/store/sqlstore/channel_store_test.go +++ b/store/sqlstore/channel_store_test.go @@ -14,7 +14,7 @@ import ( ) func TestChannelStore(t *testing.T) { - StoreTest(t, storetest.TestChannelStore) + StoreTestWithSqlSupplier(t, storetest.TestChannelStore) } func TestChannelStoreInternalDataTypes(t *testing.T) { diff --git a/store/sqlstore/store.go b/store/sqlstore/store.go index 500f98235..df912028b 100644 --- a/store/sqlstore/store.go +++ b/store/sqlstore/store.go @@ -51,6 +51,7 @@ type SqlStore interface { MarkSystemRanUnitTests() DoesTableExist(tablename string) bool DoesColumnExist(tableName string, columName string) bool + DoesTriggerExist(triggerName string) bool CreateColumnIfNotExists(tableName string, columnName string, mySqlColType string, postgresColType string, defaultValue string) bool CreateColumnIfNotExistsNoDefault(tableName string, columnName string, mySqlColType string, postgresColType string) bool RemoveColumnIfExists(tableName string, columnName string) bool diff --git a/store/sqlstore/store_test.go b/store/sqlstore/store_test.go index 58065d65d..55002aee2 100644 --- a/store/sqlstore/store_test.go +++ b/store/sqlstore/store_test.go @@ -16,10 +16,11 @@ import ( ) var storeTypes = []*struct { - Name string - Func func() (*storetest.RunningContainer, *model.SqlSettings, error) - Container *storetest.RunningContainer - Store store.Store + Name string + Func func() (*storetest.RunningContainer, *model.SqlSettings, error) + Container *storetest.RunningContainer + SqlSupplier *SqlSupplier + Store store.Store }{ { Name: "MySQL", @@ -44,6 +45,19 @@ func StoreTest(t *testing.T, f func(*testing.T, store.Store)) { } } +func StoreTestWithSqlSupplier(t *testing.T, f func(*testing.T, store.Store, storetest.SqlSupplier)) { + defer func() { + if err := recover(); err != nil { + tearDownStores() + panic(err) + } + }() + for _, st := range storeTypes { + st := st + t.Run(st.Name, func(t *testing.T) { f(t, st.Store, st.SqlSupplier) }) + } +} + func initStores() { defer func() { if err := recover(); err != nil { @@ -64,7 +78,8 @@ func initStores() { return } st.Container = container - st.Store = store.NewLayeredStore(NewSqlSupplier(*settings, nil), nil, nil) + st.SqlSupplier = NewSqlSupplier(*settings, nil) + st.Store = store.NewLayeredStore(st.SqlSupplier, nil, nil) st.Store.MarkSystemRanUnitTests() }() } diff --git a/store/sqlstore/supplier.go b/store/sqlstore/supplier.go index 6c49d91fb..d1d7564f7 100644 --- a/store/sqlstore/supplier.go +++ b/store/sqlstore/supplier.go @@ -33,6 +33,7 @@ const ( ) const ( + EXIT_GENERIC_FAILURE = 1 EXIT_CREATE_TABLE = 100 EXIT_DB_OPEN = 101 EXIT_PING = 102 @@ -116,8 +117,13 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter supplier.initConnection() + enableExperimentalPublicChannelsMaterialization := true + if settings.EnablePublicChannelsMaterialization != nil && !*settings.EnablePublicChannelsMaterialization { + enableExperimentalPublicChannelsMaterialization = false + } + supplier.oldStores.team = NewSqlTeamStore(supplier) - supplier.oldStores.channel = NewSqlChannelStore(supplier, metrics) + supplier.oldStores.channel = NewSqlChannelStoreExperimental(supplier, metrics, enableExperimentalPublicChannelsMaterialization) supplier.oldStores.post = NewSqlPostStore(supplier, metrics) supplier.oldStores.user = NewSqlUserStore(supplier, metrics) supplier.oldStores.audit = NewSqlAuditStore(supplier) @@ -151,10 +157,19 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter os.Exit(EXIT_CREATE_TABLE) } + // This store's triggers should exist before the migration is run to ensure the + // corresponding tables stay in sync. Whether or not a trigger should be created before + // or after a migration is likely to be decided on a case-by-case basis. + if err := supplier.oldStores.channel.(*SqlChannelStoreExperimental).CreateTriggersIfNotExists(); err != nil { + mlog.Critical("Error creating triggers", mlog.Err(err)) + time.Sleep(time.Second) + os.Exit(EXIT_GENERIC_FAILURE) + } + UpgradeDatabase(supplier) supplier.oldStores.team.(*SqlTeamStore).CreateIndexesIfNotExists() - supplier.oldStores.channel.(*SqlChannelStore).CreateIndexesIfNotExists() + supplier.oldStores.channel.(*SqlChannelStoreExperimental).CreateIndexesIfNotExists() supplier.oldStores.post.(*SqlPostStore).CreateIndexesIfNotExists() supplier.oldStores.user.(*SqlUserStore).CreateIndexesIfNotExists() supplier.oldStores.audit.(*SqlAuditStore).CreateIndexesIfNotExists() @@ -461,6 +476,52 @@ func (ss *SqlSupplier) DoesColumnExist(tableName string, columnName string) bool } } +func (ss *SqlSupplier) DoesTriggerExist(triggerName string) bool { + if ss.DriverName() == model.DATABASE_DRIVER_POSTGRES { + count, err := ss.GetMaster().SelectInt(` + SELECT + COUNT(0) + FROM + pg_trigger + WHERE + tgname = $1 + `, triggerName) + + if err != nil { + mlog.Critical(fmt.Sprintf("Failed to check if trigger exists %v", err)) + time.Sleep(time.Second) + os.Exit(EXIT_GENERIC_FAILURE) + } + + return count > 0 + + } else if ss.DriverName() == model.DATABASE_DRIVER_MYSQL { + count, err := ss.GetMaster().SelectInt(` + SELECT + COUNT(0) + FROM + information_schema.triggers + WHERE + trigger_schema = DATABASE() + AND trigger_name = ? + `, triggerName) + + if err != nil { + mlog.Critical(fmt.Sprintf("Failed to check if trigger exists %v", err)) + time.Sleep(time.Second) + os.Exit(EXIT_GENERIC_FAILURE) + } + + return count > 0 + + } else { + mlog.Critical("Failed to check if column exists because of missing driver") + time.Sleep(time.Second) + os.Exit(EXIT_GENERIC_FAILURE) + return false + } +} + func (ss *SqlSupplier) CreateColumnIfNotExists(tableName string, columnName string, mySqlColType string, postgresColType string, defaultValue string) bool { if ss.DoesColumnExist(tableName, columnName) { diff --git a/store/sqlstore/team_store.go b/store/sqlstore/team_store.go index d9e33df76..3ea6feced 100644 --- a/store/sqlstore/team_store.go +++ b/store/sqlstore/team_store.go @@ -851,7 +851,7 @@ func (s SqlTeamStore) ClearAllCustomRoleAssignments() store.StoreChannel { lastUserId := strings.Repeat("0", 26) lastTeamId := strings.Repeat("0", 26) - for true { + for { var transaction *gorp.Transaction var err error diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index cd45dfcb3..a8be96172 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -15,6 +15,7 @@ import ( ) const ( + VERSION_5_4_0 = "5.4.0" VERSION_5_3_0 = "5.3.0" VERSION_5_2_0 = "5.2.0" VERSION_5_1_0 = "5.1.0" @@ -84,6 +85,7 @@ func UpgradeDatabase(sqlStore SqlStore) { UpgradeDatabaseToVersion51(sqlStore) UpgradeDatabaseToVersion52(sqlStore) UpgradeDatabaseToVersion53(sqlStore) + UpgradeDatabaseToVersion54(sqlStore) // If the SchemaVersion is empty this this is the first time it has ran // so lets set it to the current version. @@ -488,3 +490,17 @@ func UpgradeDatabaseToVersion53(sqlStore SqlStore) { saveSchemaVersion(sqlStore, VERSION_5_3_0) } } + +func UpgradeDatabaseToVersion54(sqlStore SqlStore) { + // TODO: Uncomment following condition when version 5.4.0 is released + // if shouldPerformUpgrade(sqlStore, VERSION_5_3_0, VERSION_5_4_0) { + sqlStore.AlterColumnTypeIfExists("OutgoingWebhooks", "Description", "varchar(500)", "varchar(500)") + sqlStore.AlterColumnTypeIfExists("IncomingWebhooks", "Description", "varchar(500)", "varchar(500)") + + if err := sqlStore.Channel().MigratePublicChannels(); err != nil { + mlog.Critical("Failed to migrate PublicChannels table", mlog.Err(err)) + time.Sleep(time.Second) + os.Exit(EXIT_GENERIC_FAILURE) + } + // saveSchemaVersion(sqlStore, VERSION_5_4_0) +} diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index d8a77cd9d..c89c445ad 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -1255,7 +1255,7 @@ func (us SqlUserStore) ClearAllCustomRoleAssignments() store.StoreChannel { builtInRoles := model.MakeDefaultRoles() lastUserId := strings.Repeat("0", 26) - for true { + for { var transaction *gorp.Transaction var err error diff --git a/store/sqlstore/webhook_store.go b/store/sqlstore/webhook_store.go index f3c572aaf..94eadf836 100644 --- a/store/sqlstore/webhook_store.go +++ b/store/sqlstore/webhook_store.go @@ -47,7 +47,7 @@ func NewSqlWebhookStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) table.ColMap("ChannelId").SetMaxSize(26) table.ColMap("TeamId").SetMaxSize(26) table.ColMap("DisplayName").SetMaxSize(64) - table.ColMap("Description").SetMaxSize(128) + table.ColMap("Description").SetMaxSize(500) tableo := db.AddTableWithName(model.OutgoingWebhook{}, "OutgoingWebhooks").SetKeys(false, "Id") tableo.ColMap("Id").SetMaxSize(26) @@ -58,7 +58,7 @@ func NewSqlWebhookStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) tableo.ColMap("TriggerWords").SetMaxSize(1024) tableo.ColMap("CallbackURLs").SetMaxSize(1024) tableo.ColMap("DisplayName").SetMaxSize(64) - tableo.ColMap("Description").SetMaxSize(128) + tableo.ColMap("Description").SetMaxSize(500) tableo.ColMap("ContentType").SetMaxSize(128) tableo.ColMap("TriggerWhen").SetMaxSize(1) tableo.ColMap("Username").SetMaxSize(64) diff --git a/store/store.go b/store/store.go index 0c89a0a91..8c731f8d5 100644 --- a/store/store.go +++ b/store/store.go @@ -162,6 +162,7 @@ type ChannelStore interface { AnalyticsTypeCount(teamId string, channelType string) StoreChannel GetMembersForUser(teamId string, userId string) StoreChannel AutocompleteInTeam(teamId string, term string, includeDeleted bool) StoreChannel + AutocompleteInTeamForSearch(teamId string, userId string, term string, includeDeleted bool) StoreChannel SearchInTeam(teamId string, term string, includeDeleted bool) StoreChannel SearchMore(userId string, teamId string, term string) StoreChannel GetMembersByIds(channelId string, userIds []string) StoreChannel @@ -173,6 +174,11 @@ type ChannelStore interface { ResetAllChannelSchemes() StoreChannel ClearAllCustomRoleAssignments() StoreChannel ResetLastPostAt() StoreChannel + MigratePublicChannels() error + DropPublicChannels() error + EnableExperimentalPublicChannelsMaterialization() + DisableExperimentalPublicChannelsMaterialization() + IsExperimentalPublicChannelsMaterializationEnabled() bool } type ChannelMemberHistoryStore interface { diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index c827a4226..11e058f70 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -12,51 +12,74 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/mattermost/gorp" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" ) -func TestChannelStore(t *testing.T, ss store.Store) { +type SqlSupplier interface { + GetMaster() *gorp.DbMap +} + +func TestChannelStore(t *testing.T, ss store.Store, s SqlSupplier) { createDefaultRoles(t, ss) - t.Run("Save", func(t *testing.T) { testChannelStoreSave(t, ss) }) - t.Run("SaveDirectChannel", func(t *testing.T) { testChannelStoreSaveDirectChannel(t, ss) }) - t.Run("CreateDirectChannel", func(t *testing.T) { testChannelStoreCreateDirectChannel(t, ss) }) - t.Run("Update", func(t *testing.T) { testChannelStoreUpdate(t, ss) }) - t.Run("GetChannelUnread", func(t *testing.T) { testGetChannelUnread(t, ss) }) - t.Run("Get", func(t *testing.T) { testChannelStoreGet(t, ss) }) - t.Run("GetForPost", func(t *testing.T) { testChannelStoreGetForPost(t, ss) }) - t.Run("Restore", func(t *testing.T) { testChannelStoreRestore(t, ss) }) - t.Run("Delete", func(t *testing.T) { testChannelStoreDelete(t, ss) }) - t.Run("GetByName", func(t *testing.T) { testChannelStoreGetByName(t, ss) }) - t.Run("GetByNames", func(t *testing.T) { testChannelStoreGetByNames(t, ss) }) - t.Run("GetDeletedByName", func(t *testing.T) { testChannelStoreGetDeletedByName(t, ss) }) - t.Run("GetDeleted", func(t *testing.T) { testChannelStoreGetDeleted(t, ss) }) - t.Run("ChannelMemberStore", func(t *testing.T) { testChannelMemberStore(t, ss) }) - t.Run("ChannelDeleteMemberStore", func(t *testing.T) { testChannelDeleteMemberStore(t, ss) }) - t.Run("GetChannels", func(t *testing.T) { testChannelStoreGetChannels(t, ss) }) - t.Run("GetMoreChannels", func(t *testing.T) { testChannelStoreGetMoreChannels(t, ss) }) - t.Run("GetPublicChannelsForTeam", func(t *testing.T) { testChannelStoreGetPublicChannelsForTeam(t, ss) }) - t.Run("GetPublicChannelsByIdsForTeam", func(t *testing.T) { testChannelStoreGetPublicChannelsByIdsForTeam(t, ss) }) - t.Run("GetChannelCounts", func(t *testing.T) { testChannelStoreGetChannelCounts(t, ss) }) - t.Run("GetMembersForUser", func(t *testing.T) { testChannelStoreGetMembersForUser(t, ss) }) - t.Run("UpdateLastViewedAt", func(t *testing.T) { testChannelStoreUpdateLastViewedAt(t, ss) }) - t.Run("IncrementMentionCount", func(t *testing.T) { testChannelStoreIncrementMentionCount(t, ss) }) - t.Run("UpdateChannelMember", func(t *testing.T) { testUpdateChannelMember(t, ss) }) - t.Run("GetMember", func(t *testing.T) { testGetMember(t, ss) }) - t.Run("GetMemberForPost", func(t *testing.T) { testChannelStoreGetMemberForPost(t, ss) }) - t.Run("GetMemberCount", func(t *testing.T) { testGetMemberCount(t, ss) }) - t.Run("SearchMore", func(t *testing.T) { testChannelStoreSearchMore(t, ss) }) - t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, ss) }) - t.Run("GetMembersByIds", func(t *testing.T) { testChannelStoreGetMembersByIds(t, ss) }) - t.Run("AnalyticsDeletedTypeCount", func(t *testing.T) { testChannelStoreAnalyticsDeletedTypeCount(t, ss) }) - t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) }) - t.Run("MaxChannelsPerTeam", func(t *testing.T) { testChannelStoreMaxChannelsPerTeam(t, ss) }) - t.Run("GetChannelsByScheme", func(t *testing.T) { testChannelStoreGetChannelsByScheme(t, ss) }) - t.Run("MigrateChannelMembers", func(t *testing.T) { testChannelStoreMigrateChannelMembers(t, ss) }) - t.Run("ResetAllChannelSchemes", func(t *testing.T) { testResetAllChannelSchemes(t, ss) }) - t.Run("ClearAllCustomRoleAssignments", func(t *testing.T) { testChannelStoreClearAllCustomRoleAssignments(t, ss) }) + for _, enabled := range []bool{true, false} { + description := "experimental materialization" + if enabled { + description += " enabled" + ss.Channel().EnableExperimentalPublicChannelsMaterialization() + } else { + description += " disabled" + ss.Channel().DisableExperimentalPublicChannelsMaterialization() + + // Additionally drop the public channels table and all associated triggers + // to prove that the experimental store is fully disabled. + ss.Channel().DropPublicChannels() + } + t.Run(description, func(t *testing.T) { + t.Run("Save", func(t *testing.T) { testChannelStoreSave(t, ss) }) + t.Run("SaveDirectChannel", func(t *testing.T) { testChannelStoreSaveDirectChannel(t, ss) }) + t.Run("CreateDirectChannel", func(t *testing.T) { testChannelStoreCreateDirectChannel(t, ss) }) + t.Run("Update", func(t *testing.T) { testChannelStoreUpdate(t, ss) }) + t.Run("GetChannelUnread", func(t *testing.T) { testGetChannelUnread(t, ss) }) + t.Run("Get", func(t *testing.T) { testChannelStoreGet(t, ss) }) + t.Run("GetForPost", func(t *testing.T) { testChannelStoreGetForPost(t, ss) }) + t.Run("Restore", func(t *testing.T) { testChannelStoreRestore(t, ss) }) + t.Run("Delete", func(t *testing.T) { testChannelStoreDelete(t, ss) }) + t.Run("GetByName", func(t *testing.T) { testChannelStoreGetByName(t, ss) }) + t.Run("GetByNames", func(t *testing.T) { testChannelStoreGetByNames(t, ss) }) + t.Run("GetDeletedByName", func(t *testing.T) { testChannelStoreGetDeletedByName(t, ss) }) + t.Run("GetDeleted", func(t *testing.T) { testChannelStoreGetDeleted(t, ss) }) + t.Run("ChannelMemberStore", func(t *testing.T) { testChannelMemberStore(t, ss) }) + t.Run("ChannelDeleteMemberStore", func(t *testing.T) { testChannelDeleteMemberStore(t, ss) }) + t.Run("GetChannels", func(t *testing.T) { testChannelStoreGetChannels(t, ss) }) + t.Run("GetMoreChannels", func(t *testing.T) { testChannelStoreGetMoreChannels(t, ss) }) + t.Run("GetPublicChannelsForTeam", func(t *testing.T) { testChannelStoreGetPublicChannelsForTeam(t, ss) }) + t.Run("GetPublicChannelsByIdsForTeam", func(t *testing.T) { testChannelStoreGetPublicChannelsByIdsForTeam(t, ss) }) + t.Run("GetChannelCounts", func(t *testing.T) { testChannelStoreGetChannelCounts(t, ss) }) + t.Run("GetMembersForUser", func(t *testing.T) { testChannelStoreGetMembersForUser(t, ss) }) + t.Run("UpdateLastViewedAt", func(t *testing.T) { testChannelStoreUpdateLastViewedAt(t, ss) }) + t.Run("IncrementMentionCount", func(t *testing.T) { testChannelStoreIncrementMentionCount(t, ss) }) + t.Run("UpdateChannelMember", func(t *testing.T) { testUpdateChannelMember(t, ss) }) + t.Run("GetMember", func(t *testing.T) { testGetMember(t, ss) }) + t.Run("GetMemberForPost", func(t *testing.T) { testChannelStoreGetMemberForPost(t, ss) }) + t.Run("GetMemberCount", func(t *testing.T) { testGetMemberCount(t, ss) }) + t.Run("SearchMore", func(t *testing.T) { testChannelStoreSearchMore(t, ss) }) + t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, ss) }) + t.Run("AutocompleteInTeamForSearch", func(t *testing.T) { testChannelStoreAutocompleteInTeamForSearch(t, ss) }) + t.Run("GetMembersByIds", func(t *testing.T) { testChannelStoreGetMembersByIds(t, ss) }) + t.Run("AnalyticsDeletedTypeCount", func(t *testing.T) { testChannelStoreAnalyticsDeletedTypeCount(t, ss) }) + t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) }) + t.Run("MaxChannelsPerTeam", func(t *testing.T) { testChannelStoreMaxChannelsPerTeam(t, ss) }) + t.Run("GetChannelsByScheme", func(t *testing.T) { testChannelStoreGetChannelsByScheme(t, ss) }) + t.Run("MigrateChannelMembers", func(t *testing.T) { testChannelStoreMigrateChannelMembers(t, ss) }) + t.Run("ResetAllChannelSchemes", func(t *testing.T) { testResetAllChannelSchemes(t, ss) }) + t.Run("ClearAllCustomRoleAssignments", func(t *testing.T) { testChannelStoreClearAllCustomRoleAssignments(t, ss) }) + t.Run("MaterializedPublicChannels", func(t *testing.T) { testMaterializedPublicChannels(t, ss, s) }) + }) + } } func testChannelStoreSave(t *testing.T, ss store.Store) { @@ -190,8 +213,11 @@ func testChannelStoreCreateDirectChannel(t *testing.T, ss store.Store) { if res.Err != nil { t.Fatal("couldn't create direct channel", res.Err) } - c1 := res.Data.(*model.Channel) + defer func() { + <-ss.Channel().PermanentDeleteMembersByChannel(c1.Id) + <-ss.Channel().PermanentDelete(c1.Id) + }() members := (<-ss.Channel().GetMembers(c1.Id, 0, 100)).Data.(*model.ChannelMembers) if len(*members) != 2 { @@ -500,6 +526,7 @@ func testChannelStoreDelete(t *testing.T, ss store.Store) { } cresult := <-ss.Channel().GetChannels(o1.TeamId, m1.UserId, false) + require.Nil(t, cresult.Err) list := cresult.Data.(*model.ChannelList) if len(*list) != 1 { @@ -507,18 +534,21 @@ func testChannelStoreDelete(t *testing.T, ss store.Store) { } cresult = <-ss.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 0, 100) + require.Nil(t, cresult.Err) list = cresult.Data.(*model.ChannelList) if len(*list) != 1 { t.Fatal("invalid number of channels") } - <-ss.Channel().PermanentDelete(o2.Id) + cresult = <-ss.Channel().PermanentDelete(o2.Id) + require.Nil(t, cresult.Err) cresult = <-ss.Channel().GetChannels(o1.TeamId, m1.UserId, false) - t.Log(cresult.Err) - if cresult.Err.Id != "store.sql_channel.get_channels.not_found.app_error" { - t.Fatal("no channels should be found") + if assert.NotNil(t, cresult.Err) { + require.Equal(t, "store.sql_channel.get_channels.not_found.app_error", cresult.Err.Id) + } else { + require.Equal(t, &model.ChannelList{}, cresult.Data.(*model.ChannelList)) } if r := <-ss.Channel().PermanentDeleteByTeam(o1.TeamId); r.Err != nil { @@ -944,280 +974,298 @@ func testChannelStoreGetChannels(t *testing.T, ss store.Store) { } func testChannelStoreGetMoreChannels(t *testing.T, ss store.Store) { - o1 := model.Channel{} - o1.TeamId = model.NewId() - o1.DisplayName = "Channel1" - o1.Name = "zz" + model.NewId() + "b" - o1.Type = model.CHANNEL_OPEN - store.Must(ss.Channel().Save(&o1, -1)) - - o2 := model.Channel{} - o2.TeamId = model.NewId() - o2.DisplayName = "Channel2" - o2.Name = "zz" + model.NewId() + "b" - o2.Type = model.CHANNEL_OPEN - store.Must(ss.Channel().Save(&o2, -1)) - - m1 := model.ChannelMember{} - m1.ChannelId = o1.Id - m1.UserId = model.NewId() - m1.NotifyProps = model.GetDefaultChannelNotifyProps() - store.Must(ss.Channel().SaveMember(&m1)) - - m2 := model.ChannelMember{} - m2.ChannelId = o1.Id - m2.UserId = model.NewId() - m2.NotifyProps = model.GetDefaultChannelNotifyProps() - store.Must(ss.Channel().SaveMember(&m2)) - - m3 := model.ChannelMember{} - m3.ChannelId = o2.Id - m3.UserId = model.NewId() - m3.NotifyProps = model.GetDefaultChannelNotifyProps() - store.Must(ss.Channel().SaveMember(&m3)) + teamId := model.NewId() + otherTeamId := model.NewId() + userId := model.NewId() + otherUserId1 := model.NewId() + otherUserId2 := model.NewId() - o3 := model.Channel{} - o3.TeamId = o1.TeamId - o3.DisplayName = "ChannelA" - o3.Name = "zz" + model.NewId() + "b" - o3.Type = model.CHANNEL_OPEN - store.Must(ss.Channel().Save(&o3, -1)) + // o1 is a channel on the team to which the user (and the other user 1) belongs + o1 := model.Channel{ + TeamId: teamId, + DisplayName: "Channel1", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } + store.Must(ss.Channel().Save(&o1, -1)) - o4 := model.Channel{} - o4.TeamId = o1.TeamId - o4.DisplayName = "ChannelB" - o4.Name = "zz" + model.NewId() + "b" - o4.Type = model.CHANNEL_PRIVATE - store.Must(ss.Channel().Save(&o4, -1)) + store.Must(ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: o1.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + })) - o5 := model.Channel{} - o5.TeamId = o1.TeamId - o5.DisplayName = "ChannelC" - o5.Name = "zz" + model.NewId() + "b" - o5.Type = model.CHANNEL_PRIVATE - store.Must(ss.Channel().Save(&o5, -1)) + store.Must(ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: o1.Id, + UserId: otherUserId1, + NotifyProps: model.GetDefaultChannelNotifyProps(), + })) - cresult := <-ss.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 0, 100) - if cresult.Err != nil { - t.Fatal(cresult.Err) + // o2 is a channel on the other team to which the user belongs + o2 := model.Channel{ + TeamId: otherTeamId, + DisplayName: "Channel2", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, } - list := cresult.Data.(*model.ChannelList) + store.Must(ss.Channel().Save(&o2, -1)) - if len(*list) != 1 { - t.Fatal("wrong list") - } + store.Must(ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: o2.Id, + UserId: otherUserId2, + NotifyProps: model.GetDefaultChannelNotifyProps(), + })) - if (*list)[0].Name != o3.Name { - t.Fatal("missing channel") + // o3 is a channel on the team to which the user does not belong, and thus should show up + // in "more channels" + o3 := model.Channel{ + TeamId: teamId, + DisplayName: "ChannelA", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, } + store.Must(ss.Channel().Save(&o3, -1)) - o6 := model.Channel{} - o6.TeamId = o1.TeamId - o6.DisplayName = "ChannelA" - o6.Name = "zz" + model.NewId() + "b" - o6.Type = model.CHANNEL_OPEN - store.Must(ss.Channel().Save(&o6, -1)) - - cresult = <-ss.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 0, 100) - list = cresult.Data.(*model.ChannelList) - - if len(*list) != 2 { - t.Fatal("wrong list length") + // o4 is a private channel on the team to which the user does not belong + o4 := model.Channel{ + TeamId: teamId, + DisplayName: "ChannelB", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_PRIVATE, } + store.Must(ss.Channel().Save(&o4, -1)) - cresult = <-ss.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 0, 1) - list = cresult.Data.(*model.ChannelList) - - if len(*list) != 1 { - t.Fatal("wrong list length") + // o5 is another private channel on the team to which the user does belong + o5 := model.Channel{ + TeamId: teamId, + DisplayName: "ChannelC", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_PRIVATE, } + store.Must(ss.Channel().Save(&o5, -1)) - cresult = <-ss.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 1, 1) - list = cresult.Data.(*model.ChannelList) + store.Must(ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: o5.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + })) - if len(*list) != 1 { - t.Fatal("wrong list length") - } + t.Run("only o3 listed in more channels", func(t *testing.T) { + result := <-ss.Channel().GetMoreChannels(teamId, userId, 0, 100) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o3}, result.Data.(*model.ChannelList)) + }) - if r1 := <-ss.Channel().AnalyticsTypeCount(o1.TeamId, model.CHANNEL_OPEN); r1.Err != nil { - t.Fatal(r1.Err) - } else { - if r1.Data.(int64) != 3 { - t.Log(r1.Data) - t.Fatal("wrong value") - } + // o6 is another channel on the team to which the user does not belong, and would thus + // start showing up in "more channels". + o6 := model.Channel{ + TeamId: teamId, + DisplayName: "ChannelD", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, } + store.Must(ss.Channel().Save(&o6, -1)) - if r1 := <-ss.Channel().AnalyticsTypeCount(o1.TeamId, model.CHANNEL_PRIVATE); r1.Err != nil { - t.Fatal(r1.Err) - } else { - if r1.Data.(int64) != 2 { - t.Log(r1.Data) - t.Fatal("wrong value") - } + // o7 is another channel on the team to which the user does not belong, but is deleted, + // and thus would not start showing up in "more channels" + o7 := model.Channel{ + TeamId: teamId, + DisplayName: "ChannelD", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, } + store.Must(ss.Channel().Save(&o7, -1)) + store.Must(ss.Channel().Delete(o7.Id, model.GetMillis())) + + t.Run("both o3 and o6 listed in more channels", func(t *testing.T) { + result := <-ss.Channel().GetMoreChannels(teamId, userId, 0, 100) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o3, &o6}, result.Data.(*model.ChannelList)) + }) + + t.Run("only o3 listed in more channels with offset 0, limit 1", func(t *testing.T) { + result := <-ss.Channel().GetMoreChannels(teamId, userId, 0, 1) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o3}, result.Data.(*model.ChannelList)) + }) + + t.Run("only o6 listed in more channels with offset 1, limit 1", func(t *testing.T) { + result := <-ss.Channel().GetMoreChannels(teamId, userId, 1, 1) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o6}, result.Data.(*model.ChannelList)) + }) + + t.Run("verify analytics for open channels", func(t *testing.T) { + result := <-ss.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN) + require.Nil(t, result.Err) + require.EqualValues(t, 4, result.Data.(int64)) + }) + + t.Run("verify analytics for private channels", func(t *testing.T) { + result := <-ss.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE) + require.Nil(t, result.Err) + require.EqualValues(t, 2, result.Data.(int64)) + }) } func testChannelStoreGetPublicChannelsForTeam(t *testing.T, ss store.Store) { - o1 := model.Channel{} - o1.TeamId = model.NewId() - o1.DisplayName = "OpenChannel1Team1" - o1.Name = "zz" + model.NewId() + "b" - o1.Type = model.CHANNEL_OPEN - store.Must(ss.Channel().Save(&o1, -1)) - - o2 := model.Channel{} - o2.TeamId = model.NewId() - o2.DisplayName = "OpenChannel1Team2" - o2.Name = "zz" + model.NewId() + "b" - o2.Type = model.CHANNEL_OPEN - store.Must(ss.Channel().Save(&o2, -1)) - - o3 := model.Channel{} - o3.TeamId = o1.TeamId - o3.DisplayName = "PrivateChannel1Team1" - o3.Name = "zz" + model.NewId() + "b" - o3.Type = model.CHANNEL_PRIVATE - store.Must(ss.Channel().Save(&o3, -1)) - - cresult := <-ss.Channel().GetPublicChannelsForTeam(o1.TeamId, 0, 100) - if cresult.Err != nil { - t.Fatal(cresult.Err) - } - list := cresult.Data.(*model.ChannelList) - - if len(*list) != 1 { - t.Fatal("wrong list") - } + teamId := model.NewId() - if (*list)[0].Name != o1.Name { - t.Fatal("missing channel") + // o1 is a public channel on the team + o1 := model.Channel{ + TeamId: teamId, + DisplayName: "OpenChannel1Team1", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, } + store.Must(ss.Channel().Save(&o1, -1)) - o4 := model.Channel{} - o4.TeamId = o1.TeamId - o4.DisplayName = "OpenChannel2Team1" - o4.Name = "zz" + model.NewId() + "b" - o4.Type = model.CHANNEL_OPEN - store.Must(ss.Channel().Save(&o4, -1)) - - cresult = <-ss.Channel().GetPublicChannelsForTeam(o1.TeamId, 0, 100) - list = cresult.Data.(*model.ChannelList) - - if len(*list) != 2 { - t.Fatal("wrong list length") + // o2 is a public channel on another team + o2 := model.Channel{ + TeamId: model.NewId(), + DisplayName: "OpenChannel1Team2", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, } + store.Must(ss.Channel().Save(&o2, -1)) - cresult = <-ss.Channel().GetPublicChannelsForTeam(o1.TeamId, 0, 1) - list = cresult.Data.(*model.ChannelList) - - if len(*list) != 1 { - t.Fatal("wrong list length") + // o3 is a private channel on the team + o3 := model.Channel{ + TeamId: teamId, + DisplayName: "PrivateChannel1Team1", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_PRIVATE, } + store.Must(ss.Channel().Save(&o3, -1)) - cresult = <-ss.Channel().GetPublicChannelsForTeam(o1.TeamId, 1, 1) - list = cresult.Data.(*model.ChannelList) + t.Run("only o1 initially listed in public channels", func(t *testing.T) { + result := <-ss.Channel().GetPublicChannelsForTeam(teamId, 0, 100) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o1}, result.Data.(*model.ChannelList)) + }) - if len(*list) != 1 { - t.Fatal("wrong list length") - } - - if r1 := <-ss.Channel().AnalyticsTypeCount(o1.TeamId, model.CHANNEL_OPEN); r1.Err != nil { - t.Fatal(r1.Err) - } else { - if r1.Data.(int64) != 2 { - t.Log(r1.Data) - t.Fatal("wrong value") - } + // o4 is another public channel on the team + o4 := model.Channel{ + TeamId: teamId, + DisplayName: "OpenChannel2Team1", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, } + store.Must(ss.Channel().Save(&o4, -1)) - if r1 := <-ss.Channel().AnalyticsTypeCount(o1.TeamId, model.CHANNEL_PRIVATE); r1.Err != nil { - t.Fatal(r1.Err) - } else { - if r1.Data.(int64) != 1 { - t.Log(r1.Data) - t.Fatal("wrong value") - } + // o5 is another public, but deleted channel on the team + o5 := model.Channel{ + TeamId: teamId, + DisplayName: "OpenChannel3Team1", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, } + store.Must(ss.Channel().Save(&o5, -1)) + store.Must(ss.Channel().Delete(o5.Id, model.GetMillis())) + + t.Run("both o1 and o4 listed in public channels", func(t *testing.T) { + cresult := <-ss.Channel().GetPublicChannelsForTeam(teamId, 0, 100) + require.Nil(t, cresult.Err) + require.Equal(t, &model.ChannelList{&o1, &o4}, cresult.Data.(*model.ChannelList)) + }) + + t.Run("only o1 listed in public channels with offset 0, limit 1", func(t *testing.T) { + result := <-ss.Channel().GetPublicChannelsForTeam(teamId, 0, 1) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o1}, result.Data.(*model.ChannelList)) + }) + + t.Run("only o4 listed in public channels with offset 1, limit 1", func(t *testing.T) { + result := <-ss.Channel().GetPublicChannelsForTeam(teamId, 1, 1) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o4}, result.Data.(*model.ChannelList)) + }) + + t.Run("verify analytics for open channels", func(t *testing.T) { + result := <-ss.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN) + require.Nil(t, result.Err) + require.EqualValues(t, 3, result.Data.(int64)) + }) + + t.Run("verify analytics for private channels", func(t *testing.T) { + result := <-ss.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE) + require.Nil(t, result.Err) + require.EqualValues(t, 1, result.Data.(int64)) + }) } func testChannelStoreGetPublicChannelsByIdsForTeam(t *testing.T, ss store.Store) { - teamId1 := model.NewId() + teamId := model.NewId() - oc1 := model.Channel{} - oc1.TeamId = teamId1 - oc1.DisplayName = "OpenChannel1Team1" - oc1.Name = "zz" + model.NewId() + "b" - oc1.Type = model.CHANNEL_OPEN + // oc1 is a public channel on the team + oc1 := model.Channel{ + TeamId: teamId, + DisplayName: "OpenChannel1Team1", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } store.Must(ss.Channel().Save(&oc1, -1)) - oc2 := model.Channel{} - oc2.TeamId = model.NewId() - oc2.DisplayName = "OpenChannel2TeamOther" - oc2.Name = "zz" + model.NewId() + "b" - oc2.Type = model.CHANNEL_OPEN + // oc2 is a public channel on another team + oc2 := model.Channel{ + TeamId: model.NewId(), + DisplayName: "OpenChannel2TeamOther", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } store.Must(ss.Channel().Save(&oc2, -1)) - pc3 := model.Channel{} - pc3.TeamId = teamId1 - pc3.DisplayName = "PrivateChannel3Team1" - pc3.Name = "zz" + model.NewId() + "b" - pc3.Type = model.CHANNEL_PRIVATE - store.Must(ss.Channel().Save(&pc3, -1)) - - cids := []string{oc1.Id} - cresult := <-ss.Channel().GetPublicChannelsByIdsForTeam(teamId1, cids) - list := cresult.Data.(*model.ChannelList) - - if len(*list) != 1 { - t.Fatal("should return 1 channel") + // pc3 is a private channel on the team + pc3 := model.Channel{ + TeamId: teamId, + DisplayName: "PrivateChannel3Team1", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_PRIVATE, } + store.Must(ss.Channel().Save(&pc3, -1)) - if (*list)[0].Id != oc1.Id { - t.Fatal("missing channel") - } + t.Run("oc1 by itself should be found as a public channel in the team", func(t *testing.T) { + result := <-ss.Channel().GetPublicChannelsByIdsForTeam(teamId, []string{oc1.Id}) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&oc1}, result.Data.(*model.ChannelList)) + }) - cids = append(cids, oc2.Id) - cids = append(cids, model.NewId()) - cids = append(cids, pc3.Id) - cresult = <-ss.Channel().GetPublicChannelsByIdsForTeam(teamId1, cids) - list = cresult.Data.(*model.ChannelList) + t.Run("only oc1, among others, should be found as a public channel in the team", func(t *testing.T) { + result := <-ss.Channel().GetPublicChannelsByIdsForTeam(teamId, []string{oc1.Id, oc2.Id, model.NewId(), pc3.Id}) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&oc1}, result.Data.(*model.ChannelList)) + }) - if len(*list) != 1 { - t.Fatal("should return 1 channel") + // oc4 is another public channel on the team + oc4 := model.Channel{ + TeamId: teamId, + DisplayName: "OpenChannel4Team1", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, } - - oc4 := model.Channel{} - oc4.TeamId = teamId1 - oc4.DisplayName = "OpenChannel4Team1" - oc4.Name = "zz" + model.NewId() + "b" - oc4.Type = model.CHANNEL_OPEN store.Must(ss.Channel().Save(&oc4, -1)) - cids = append(cids, oc4.Id) - cresult = <-ss.Channel().GetPublicChannelsByIdsForTeam(teamId1, cids) - list = cresult.Data.(*model.ChannelList) - - if len(*list) != 2 { - t.Fatal("should return 2 channels") - } - - if (*list)[0].Id != oc1.Id { - t.Fatal("missing channel") - } - - if (*list)[1].Id != oc4.Id { - t.Fatal("missing channel") + // oc4 is another public, but deleted channel on the team + oc5 := model.Channel{ + TeamId: teamId, + DisplayName: "OpenChannel4Team1", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, } + store.Must(ss.Channel().Save(&oc5, -1)) + store.Must(ss.Channel().Delete(oc5.Id, model.GetMillis())) - cids = cids[:0] - cids = append(cids, model.NewId()) - cresult = <-ss.Channel().GetPublicChannelsByIdsForTeam(teamId1, cids) - list = cresult.Data.(*model.ChannelList) + t.Run("only oc1 and oc4, among others, should be found as a public channel in the team", func(t *testing.T) { + result := <-ss.Channel().GetPublicChannelsByIdsForTeam(teamId, []string{oc1.Id, oc2.Id, model.NewId(), pc3.Id, oc4.Id}) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&oc1, &oc4}, result.Data.(*model.ChannelList)) + }) - if len(*list) != 0 { - t.Fatal("should not return a channel") - } + t.Run("random channel id should not be found as a public channel in the team", func(t *testing.T) { + result := <-ss.Channel().GetPublicChannelsByIdsForTeam(teamId, []string{model.NewId()}) + require.NotNil(t, result.Err) + require.Equal(t, result.Err.Id, "store.sql_channel.get_channels_by_ids.not_found.app_error") + }) } func testChannelStoreGetChannelCounts(t *testing.T, ss store.Store) { @@ -1643,182 +1691,336 @@ func testGetMemberCount(t *testing.T, ss store.Store) { } func testChannelStoreSearchMore(t *testing.T, ss store.Store) { - o1 := model.Channel{} - o1.TeamId = model.NewId() - o1.DisplayName = "ChannelA" - o1.Name = "zz" + model.NewId() + "b" - o1.Type = model.CHANNEL_OPEN - store.Must(ss.Channel().Save(&o1, -1)) + teamId := model.NewId() + otherTeamId := model.NewId() - o2 := model.Channel{} - o2.TeamId = model.NewId() - o2.DisplayName = "Channel2" - o2.Name = "zz" + model.NewId() + "b" - o2.Type = model.CHANNEL_OPEN - store.Must(ss.Channel().Save(&o2, -1)) + o1 := model.Channel{ + TeamId: teamId, + DisplayName: "ChannelA", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } + store.Must(ss.Channel().Save(&o1, -1)) - m1 := model.ChannelMember{} - m1.ChannelId = o1.Id - m1.UserId = model.NewId() - m1.NotifyProps = model.GetDefaultChannelNotifyProps() + m1 := model.ChannelMember{ + ChannelId: o1.Id, + UserId: model.NewId(), + NotifyProps: model.GetDefaultChannelNotifyProps(), + } store.Must(ss.Channel().SaveMember(&m1)) - m2 := model.ChannelMember{} - m2.ChannelId = o1.Id - m2.UserId = model.NewId() - m2.NotifyProps = model.GetDefaultChannelNotifyProps() + m2 := model.ChannelMember{ + ChannelId: o1.Id, + UserId: model.NewId(), + NotifyProps: model.GetDefaultChannelNotifyProps(), + } store.Must(ss.Channel().SaveMember(&m2)) - m3 := model.ChannelMember{} - m3.ChannelId = o2.Id - m3.UserId = model.NewId() - m3.NotifyProps = model.GetDefaultChannelNotifyProps() + o2 := model.Channel{ + TeamId: otherTeamId, + DisplayName: "Channel2", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } + store.Must(ss.Channel().Save(&o2, -1)) + + m3 := model.ChannelMember{ + ChannelId: o2.Id, + UserId: model.NewId(), + NotifyProps: model.GetDefaultChannelNotifyProps(), + } store.Must(ss.Channel().SaveMember(&m3)) - o3 := model.Channel{} - o3.TeamId = o1.TeamId - o3.DisplayName = "ChannelA" - o3.Name = "zz" + model.NewId() + "b" - o3.Type = model.CHANNEL_OPEN + o3 := model.Channel{ + TeamId: teamId, + DisplayName: "ChannelA", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } store.Must(ss.Channel().Save(&o3, -1)) - o4 := model.Channel{} - o4.TeamId = o1.TeamId - o4.DisplayName = "ChannelB" - o4.Name = "zz" + model.NewId() + "b" - o4.Type = model.CHANNEL_PRIVATE + o4 := model.Channel{ + TeamId: teamId, + DisplayName: "ChannelB", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_PRIVATE, + } store.Must(ss.Channel().Save(&o4, -1)) - o5 := model.Channel{} - o5.TeamId = o1.TeamId - o5.DisplayName = "ChannelC" - o5.Name = "zz" + model.NewId() + "b" - o5.Type = model.CHANNEL_PRIVATE + o5 := model.Channel{ + TeamId: teamId, + DisplayName: "ChannelC", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_PRIVATE, + } store.Must(ss.Channel().Save(&o5, -1)) - o6 := model.Channel{} - o6.TeamId = o1.TeamId - o6.DisplayName = "Off-Topic" - o6.Name = "off-topic" - o6.Type = model.CHANNEL_OPEN + o6 := model.Channel{ + TeamId: teamId, + DisplayName: "Off-Topic", + Name: "off-topic", + Type: model.CHANNEL_OPEN, + } store.Must(ss.Channel().Save(&o6, -1)) - o7 := model.Channel{} - o7.TeamId = o1.TeamId - o7.DisplayName = "Off-Set" - o7.Name = "off-set" - o7.Type = model.CHANNEL_OPEN + o7 := model.Channel{ + TeamId: teamId, + DisplayName: "Off-Set", + Name: "off-set", + Type: model.CHANNEL_OPEN, + } store.Must(ss.Channel().Save(&o7, -1)) - o8 := model.Channel{} - o8.TeamId = o1.TeamId - o8.DisplayName = "Off-Limit" - o8.Name = "off-limit" - o8.Type = model.CHANNEL_PRIVATE + o8 := model.Channel{ + TeamId: teamId, + DisplayName: "Off-Limit", + Name: "off-limit", + Type: model.CHANNEL_PRIVATE, + } store.Must(ss.Channel().Save(&o8, -1)) - o9 := model.Channel{} - o9.TeamId = o1.TeamId - o9.DisplayName = "Channel With Purpose" - o9.Purpose = "This can now be searchable!" - o9.Name = "with-purpose" - o9.Type = model.CHANNEL_OPEN + o9 := model.Channel{ + TeamId: teamId, + DisplayName: "Channel With Purpose", + Purpose: "This can now be searchable!", + Name: "with-purpose", + Type: model.CHANNEL_OPEN, + } store.Must(ss.Channel().Save(&o9, -1)) - if result := <-ss.Channel().SearchMore(m1.UserId, o1.TeamId, "ChannelA"); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - if len(*channels) == 0 { - t.Fatal("should not be empty") - } + o10 := model.Channel{ + TeamId: teamId, + DisplayName: "ChannelA", + Name: "channel-a-deleted", + Type: model.CHANNEL_OPEN, + } + store.Must(ss.Channel().Save(&o10, -1)) + o10.DeleteAt = model.GetMillis() + o10.UpdateAt = o10.DeleteAt + store.Must(ss.Channel().Delete(o10.Id, o10.DeleteAt)) + + t.Run("three public channels matching 'ChannelA', but already a member of one and one deleted", func(t *testing.T) { + result := <-ss.Channel().SearchMore(m1.UserId, teamId, "ChannelA") + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o3}, result.Data.(*model.ChannelList)) + }) + + t.Run("one public channels, but already a member", func(t *testing.T) { + result := <-ss.Channel().SearchMore(m1.UserId, teamId, o4.Name) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{}, result.Data.(*model.ChannelList)) + }) + + t.Run("three matching channels, but only two public", func(t *testing.T) { + result := <-ss.Channel().SearchMore(m1.UserId, teamId, "off-") + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o7, &o6}, result.Data.(*model.ChannelList)) + }) + + t.Run("one channel matching 'off-topic'", func(t *testing.T) { + result := <-ss.Channel().SearchMore(m1.UserId, teamId, "off-topic") + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o6}, result.Data.(*model.ChannelList)) + }) + + t.Run("search purpose", func(t *testing.T) { + result := <-ss.Channel().SearchMore(m1.UserId, teamId, "now searchable") + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o9}, result.Data.(*model.ChannelList)) + }) +} - if (*channels)[0].Name != o3.Name { - t.Fatal("wrong channel returned") - } +type ByChannelDisplayName model.ChannelList + +func (s ByChannelDisplayName) Len() int { return len(s) } +func (s ByChannelDisplayName) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} +func (s ByChannelDisplayName) Less(i, j int) bool { + if s[i].DisplayName != s[j].DisplayName { + return s[i].DisplayName < s[j].DisplayName } - if result := <-ss.Channel().SearchMore(m1.UserId, o1.TeamId, o4.Name); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - if len(*channels) != 0 { - t.Fatal("should be empty") - } + return s[i].Id < s[j].Id +} + +func testChannelStoreSearchInTeam(t *testing.T, ss store.Store) { + teamId := model.NewId() + otherTeamId := model.NewId() + + o1 := model.Channel{ + TeamId: teamId, + DisplayName: "ChannelA", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, } + store.Must(ss.Channel().Save(&o1, -1)) - if result := <-ss.Channel().SearchMore(m1.UserId, o1.TeamId, o3.Name); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - if len(*channels) == 0 { - t.Fatal("should not be empty") - } + o2 := model.Channel{ + TeamId: otherTeamId, + DisplayName: "ChannelA", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } + store.Must(ss.Channel().Save(&o2, -1)) - if (*channels)[0].Name != o3.Name { - t.Fatal("wrong channel returned") - } + m1 := model.ChannelMember{ + ChannelId: o1.Id, + UserId: model.NewId(), + NotifyProps: model.GetDefaultChannelNotifyProps(), } + store.Must(ss.Channel().SaveMember(&m1)) - if result := <-ss.Channel().SearchMore(m1.UserId, o1.TeamId, "off-"); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - if len(*channels) != 2 { - t.Fatal("should return 2 channels, not including private channel") - } + m2 := model.ChannelMember{ + ChannelId: o1.Id, + UserId: model.NewId(), + NotifyProps: model.GetDefaultChannelNotifyProps(), + } + store.Must(ss.Channel().SaveMember(&m2)) - if (*channels)[0].Name != o7.Name { - t.Fatal("wrong channel returned") - } + m3 := model.ChannelMember{ + ChannelId: o2.Id, + UserId: model.NewId(), + NotifyProps: model.GetDefaultChannelNotifyProps(), + } + store.Must(ss.Channel().SaveMember(&m3)) - if (*channels)[1].Name != o6.Name { - t.Fatal("wrong channel returned") - } + o3 := model.Channel{ + TeamId: teamId, + DisplayName: "ChannelA (alternate)", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, } + store.Must(ss.Channel().Save(&o3, -1)) - if result := <-ss.Channel().SearchMore(m1.UserId, o1.TeamId, "off-topic"); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - if len(*channels) != 1 { - t.Fatal("should return 1 channel") - } + o4 := model.Channel{ + TeamId: teamId, + DisplayName: "Channel B", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_PRIVATE, + } + store.Must(ss.Channel().Save(&o4, -1)) - if (*channels)[0].Name != o6.Name { - t.Fatal("wrong channel returned") - } + o5 := model.Channel{ + TeamId: teamId, + DisplayName: "Channel C", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_PRIVATE, } + store.Must(ss.Channel().Save(&o5, -1)) - if result := <-ss.Channel().SearchMore(m1.UserId, o1.TeamId, "now searchable"); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - if len(*channels) != 1 { - t.Fatal("should return 1 channel") - } + o6 := model.Channel{ + TeamId: teamId, + DisplayName: "Off-Topic", + Name: "off-topic", + Type: model.CHANNEL_OPEN, + } + store.Must(ss.Channel().Save(&o6, -1)) - if (*channels)[0].Name != o9.Name { - t.Fatal("wrong channel returned") - } + o7 := model.Channel{ + TeamId: teamId, + DisplayName: "Off-Set", + Name: "off-set", + Type: model.CHANNEL_OPEN, } + store.Must(ss.Channel().Save(&o7, -1)) - /* - // Disabling this check as it will fail on PostgreSQL as we have "liberalised" channel matching to deal with - // Full-Text Stemming Limitations. - if result := <-ss.Channel().SearchMore(m1.UserId, o1.TeamId, "off-topics"); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - if len(*channels) != 0 { - t.Logf("%v\n", *channels) - t.Fatal("should be empty") - } + o8 := model.Channel{ + TeamId: teamId, + DisplayName: "Off-Limit", + Name: "off-limit", + Type: model.CHANNEL_PRIVATE, + } + store.Must(ss.Channel().Save(&o8, -1)) + + o9 := model.Channel{ + TeamId: teamId, + DisplayName: "Town Square", + Name: "town-square", + Type: model.CHANNEL_OPEN, + } + store.Must(ss.Channel().Save(&o9, -1)) + + o10 := model.Channel{ + TeamId: teamId, + DisplayName: "The", + Name: "the", + Type: model.CHANNEL_OPEN, + } + store.Must(ss.Channel().Save(&o10, -1)) + + o11 := model.Channel{ + TeamId: teamId, + DisplayName: "Native Mobile Apps", + Name: "native-mobile-apps", + Type: model.CHANNEL_OPEN, + } + store.Must(ss.Channel().Save(&o11, -1)) + + o12 := model.Channel{ + TeamId: teamId, + DisplayName: "ChannelZ", + Purpose: "This can now be searchable!", + Name: "with-purpose", + Type: model.CHANNEL_OPEN, + } + store.Must(ss.Channel().Save(&o12, -1)) + + o13 := model.Channel{ + TeamId: teamId, + DisplayName: "ChannelA (deleted)", + Name: model.NewId(), + Type: model.CHANNEL_OPEN, + } + store.Must(ss.Channel().Save(&o13, -1)) + o13.DeleteAt = model.GetMillis() + o13.UpdateAt = o13.DeleteAt + store.Must(ss.Channel().Delete(o13.Id, o13.DeleteAt)) + + testCases := []struct { + Description string + TeamId string + Term string + IncludeDeleted bool + ExpectedResults *model.ChannelList + }{ + {"ChannelA", teamId, "ChannelA", false, &model.ChannelList{&o1, &o3}}, + {"ChannelA, include deleted", teamId, "ChannelA", true, &model.ChannelList{&o1, &o3, &o13}}, + {"ChannelA, other team", otherTeamId, "ChannelA", false, &model.ChannelList{&o2}}, + {"empty string", teamId, "", false, &model.ChannelList{&o1, &o3, &o12, &o11, &o7, &o6, &o10, &o9}}, + {"no matches", teamId, "blargh", false, &model.ChannelList{}}, + {"prefix", teamId, "off-", false, &model.ChannelList{&o7, &o6}}, + {"full match with dash", teamId, "off-topic", false, &model.ChannelList{&o6}}, + {"town square", teamId, "town square", false, &model.ChannelList{&o9}}, + {"the in name", teamId, "the", false, &model.ChannelList{&o10}}, + {"Mobile", teamId, "Mobile", false, &model.ChannelList{&o11}}, + {"search purpose", teamId, "now searchable", false, &model.ChannelList{&o12}}, + {"pipe ignored", teamId, "town square |", false, &model.ChannelList{&o9}}, + } + + for name, search := range map[string]func(teamId string, term string, includeDeleted bool) store.StoreChannel{ + "AutocompleteInTeam": ss.Channel().AutocompleteInTeam, + "SearchInTeam": ss.Channel().SearchInTeam, + } { + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + result := <-search(testCase.TeamId, testCase.Term, testCase.IncludeDeleted) + require.Nil(t, result.Err) + + channels := result.Data.(*model.ChannelList) + + // AutoCompleteInTeam doesn't currently sort its output results. + if name == "AutocompleteInTeam" { + sort.Sort(ByChannelDisplayName(*channels)) + } + + require.Equal(t, testCase.ExpectedResults, channels) + }) } - */ + } } -func testChannelStoreSearchInTeam(t *testing.T, ss store.Store) { +func testChannelStoreAutocompleteInTeamForSearch(t *testing.T, ss store.Store) { o1 := model.Channel{} o1.TeamId = model.NewId() o1.DisplayName = "ChannelA" @@ -1826,6 +2028,12 @@ func testChannelStoreSearchInTeam(t *testing.T, ss store.Store) { o1.Type = model.CHANNEL_OPEN store.Must(ss.Channel().Save(&o1, -1)) + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = model.NewId() + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + store.Must(ss.Channel().SaveMember(&m1)) + o2 := model.Channel{} o2.TeamId = model.NewId() o2.DisplayName = "Channel2" @@ -1833,24 +2041,12 @@ func testChannelStoreSearchInTeam(t *testing.T, ss store.Store) { o2.Type = model.CHANNEL_OPEN store.Must(ss.Channel().Save(&o2, -1)) - m1 := model.ChannelMember{} - m1.ChannelId = o1.Id - m1.UserId = model.NewId() - m1.NotifyProps = model.GetDefaultChannelNotifyProps() - store.Must(ss.Channel().SaveMember(&m1)) - m2 := model.ChannelMember{} - m2.ChannelId = o1.Id - m2.UserId = model.NewId() + m2.ChannelId = o2.Id + m2.UserId = m1.UserId m2.NotifyProps = model.GetDefaultChannelNotifyProps() store.Must(ss.Channel().SaveMember(&m2)) - m3 := model.ChannelMember{} - m3.ChannelId = o2.Id - m3.UserId = model.NewId() - m3.NotifyProps = model.GetDefaultChannelNotifyProps() - store.Must(ss.Channel().SaveMember(&m3)) - o3 := model.Channel{} o3.TeamId = o1.TeamId o3.DisplayName = "ChannelA" @@ -1858,13 +2054,27 @@ func testChannelStoreSearchInTeam(t *testing.T, ss store.Store) { o3.Type = model.CHANNEL_OPEN store.Must(ss.Channel().Save(&o3, -1)) + m3 := model.ChannelMember{} + m3.ChannelId = o3.Id + m3.UserId = m1.UserId + m3.NotifyProps = model.GetDefaultChannelNotifyProps() + store.Must(ss.Channel().SaveMember(&m3)) + + store.Must(ss.Channel().SetDeleteAt(o3.Id, 100, 100)) + o4 := model.Channel{} o4.TeamId = o1.TeamId - o4.DisplayName = "ChannelB" + o4.DisplayName = "ChannelA" o4.Name = "zz" + model.NewId() + "b" o4.Type = model.CHANNEL_PRIVATE store.Must(ss.Channel().Save(&o4, -1)) + m4 := model.ChannelMember{} + m4.ChannelId = o4.Id + m4.UserId = m1.UserId + m4.NotifyProps = model.GetDefaultChannelNotifyProps() + store.Must(ss.Channel().SaveMember(&m4)) + o5 := model.Channel{} o5.TeamId = o1.TeamId o5.DisplayName = "ChannelC" @@ -1872,184 +2082,26 @@ func testChannelStoreSearchInTeam(t *testing.T, ss store.Store) { o5.Type = model.CHANNEL_PRIVATE store.Must(ss.Channel().Save(&o5, -1)) - o6 := model.Channel{} - o6.TeamId = o1.TeamId - o6.DisplayName = "Off-Topic" - o6.Name = "off-topic" - o6.Type = model.CHANNEL_OPEN - store.Must(ss.Channel().Save(&o6, -1)) - - o7 := model.Channel{} - o7.TeamId = o1.TeamId - o7.DisplayName = "Off-Set" - o7.Name = "off-set" - o7.Type = model.CHANNEL_OPEN - store.Must(ss.Channel().Save(&o7, -1)) - - o8 := model.Channel{} - o8.TeamId = o1.TeamId - o8.DisplayName = "Off-Limit" - o8.Name = "off-limit" - o8.Type = model.CHANNEL_PRIVATE - store.Must(ss.Channel().Save(&o8, -1)) - - o9 := model.Channel{} - o9.TeamId = o1.TeamId - o9.DisplayName = "Town Square" - o9.Name = "town-square" - o9.Type = model.CHANNEL_OPEN - store.Must(ss.Channel().Save(&o9, -1)) - - o10 := model.Channel{} - o10.TeamId = o1.TeamId - o10.DisplayName = "The" - o10.Name = "the" - o10.Type = model.CHANNEL_OPEN - store.Must(ss.Channel().Save(&o10, -1)) - - o11 := model.Channel{} - o11.TeamId = o1.TeamId - o11.DisplayName = "Native Mobile Apps" - o11.Name = "native-mobile-apps" - o11.Type = model.CHANNEL_OPEN - store.Must(ss.Channel().Save(&o11, -1)) - - o12 := model.Channel{} - o12.TeamId = o1.TeamId - o12.DisplayName = "Channel With Purpose" - o12.Purpose = "This can now be searchable!" - o12.Name = "with-purpose" - o12.Type = model.CHANNEL_OPEN - store.Must(ss.Channel().Save(&o12, -1)) - - for name, search := range map[string]func(teamId string, term string, includeDeleted bool) store.StoreChannel{ - "AutocompleteInTeam": ss.Channel().AutocompleteInTeam, - "SearchInTeam": ss.Channel().SearchInTeam, - } { - t.Run(name, func(t *testing.T) { - if result := <-search(o1.TeamId, "ChannelA", false); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - if len(*channels) != 2 { - t.Fatal("wrong length") - } - } - - if result := <-search(o1.TeamId, "", false); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - if len(*channels) == 0 { - t.Fatal("should not be empty") - } - } - - if result := <-search(o1.TeamId, "blargh", false); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - if len(*channels) != 0 { - t.Fatal("should be empty") - } - } - - if result := <-search(o1.TeamId, "off-", false); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - if len(*channels) != 2 { - t.Fatal("should return 2 channels, not including private channel") - } - - if (*channels)[0].Name != o7.Name { - t.Fatal("wrong channel returned") - } - - if (*channels)[1].Name != o6.Name { - t.Fatal("wrong channel returned") - } - } - - if result := <-search(o1.TeamId, "off-topic", false); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - if len(*channels) != 1 { - t.Fatal("should return 1 channel") - } - - if (*channels)[0].Name != o6.Name { - t.Fatal("wrong channel returned") - } - } - - if result := <-search(o1.TeamId, "town square", false); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - if len(*channels) != 1 { - t.Fatal("should return 1 channel") - } - - if (*channels)[0].Name != o9.Name { - t.Fatal("wrong channel returned") - } - } - - if result := <-search(o1.TeamId, "the", false); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - t.Log(channels.ToJson()) - if len(*channels) != 1 { - t.Fatal("should return 1 channel") - } - - if (*channels)[0].Name != o10.Name { - t.Fatal("wrong channel returned") - } - } - - if result := <-search(o1.TeamId, "Mobile", false); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - t.Log(channels.ToJson()) - if len(*channels) != 1 { - t.Fatal("should return 1 channel") - } - - if (*channels)[0].Name != o11.Name { - t.Fatal("wrong channel returned") - } - } - - if result := <-search(o1.TeamId, "now searchable", false); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - if len(*channels) != 1 { - t.Fatal("should return 1 channel") - } - - if (*channels)[0].Name != o12.Name { - t.Fatal("wrong channel returned") - } - } - - if result := <-search(o1.TeamId, "town square |", false); result.Err != nil { - t.Fatal(result.Err) - } else { - channels := result.Data.(*model.ChannelList) - if len(*channels) != 1 { - t.Fatal("should return 1 channel") - } - - if (*channels)[0].Name != o9.Name { - t.Fatal("wrong channel returned") - } - } + tt := []struct { + name string + term string + includeDeleted bool + expectedMatches int + }{ + {"Empty search (list all)", "", false, 3}, + {"Narrow search", "ChannelA", false, 2}, + {"Wide search", "Cha", false, 3}, + {"Wide search with archived channels", "Cha", true, 4}, + {"Narrow with archived channels", "ChannelA", true, 3}, + {"Search without results", "blarg", true, 0}, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + result := <-ss.Channel().AutocompleteInTeamForSearch(o1.TeamId, m1.UserId, "ChannelA", false) + require.Nil(t, result.Err) + channels := result.Data.(*model.ChannelList) + require.Len(t, *channels, 2) }) } } @@ -2135,6 +2187,10 @@ func testChannelStoreAnalyticsDeletedTypeCount(t *testing.T, ss store.Store) { } else { d4 = result.Data.(*model.Channel) } + defer func() { + <-ss.Channel().PermanentDeleteMembersByChannel(d4.Id) + <-ss.Channel().PermanentDelete(d4.Id) + }() var openStartCount int64 if result := <-ss.Channel().AnalyticsDeletedTypeCount("", "O"); result.Err != nil { @@ -2287,9 +2343,9 @@ func testChannelStoreGetChannelsByScheme(t *testing.T, ss store.Store) { Type: model.CHANNEL_OPEN, } - c1 = (<-ss.Channel().Save(c1, 100)).Data.(*model.Channel) - c2 = (<-ss.Channel().Save(c2, 100)).Data.(*model.Channel) - c3 = (<-ss.Channel().Save(c3, 100)).Data.(*model.Channel) + _ = (<-ss.Channel().Save(c1, 100)).Data.(*model.Channel) + _ = (<-ss.Channel().Save(c2, 100)).Data.(*model.Channel) + _ = (<-ss.Channel().Save(c3, 100)).Data.(*model.Channel) // Get the channels by a valid Scheme ID. res1 := <-ss.Channel().GetChannelsByScheme(s1.Id, 0, 100) @@ -2482,3 +2538,158 @@ func testChannelStoreClearAllCustomRoleAssignments(t *testing.T, ss store.Store) require.Nil(t, r4.Err) assert.Equal(t, "", r4.Data.(*model.ChannelMember).Roles) } + +// testMaterializedPublicChannels tests edge cases involving the triggers and stored procedures +// that materialize the PublicChannels table. +func testMaterializedPublicChannels(t *testing.T, ss store.Store, s SqlSupplier) { + if !ss.Channel().IsExperimentalPublicChannelsMaterializationEnabled() { + return + } + + teamId := model.NewId() + + // o1 is a public channel on the team + o1 := model.Channel{ + TeamId: teamId, + DisplayName: "Open Channel", + Name: model.NewId(), + Type: model.CHANNEL_OPEN, + } + store.Must(ss.Channel().Save(&o1, -1)) + + // o2 is another public channel on the team + o2 := model.Channel{ + TeamId: teamId, + DisplayName: "Open Channel 2", + Name: model.NewId(), + Type: model.CHANNEL_OPEN, + } + store.Must(ss.Channel().Save(&o2, -1)) + + t.Run("o1 and o2 initially listed in public channels", func(t *testing.T) { + result := <-ss.Channel().SearchInTeam(teamId, "", true) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o1, &o2}, result.Data.(*model.ChannelList)) + }) + + o1.DeleteAt = model.GetMillis() + o1.UpdateAt = model.GetMillis() + store.Must(ss.Channel().Delete(o1.Id, o1.DeleteAt)) + + t.Run("o1 still listed in public channels when marked as deleted", func(t *testing.T) { + result := <-ss.Channel().SearchInTeam(teamId, "", true) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o1, &o2}, result.Data.(*model.ChannelList)) + }) + + <-ss.Channel().PermanentDelete(o1.Id) + + t.Run("o1 no longer listed in public channels when permanently deleted", func(t *testing.T) { + result := <-ss.Channel().SearchInTeam(teamId, "", true) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o2}, result.Data.(*model.ChannelList)) + }) + + o2.Type = model.CHANNEL_PRIVATE + require.Nil(t, (<-ss.Channel().Update(&o2)).Err) + + t.Run("o2 no longer listed since now private", func(t *testing.T) { + result := <-ss.Channel().SearchInTeam(teamId, "", true) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{}, result.Data.(*model.ChannelList)) + }) + + o2.Type = model.CHANNEL_OPEN + require.Nil(t, (<-ss.Channel().Update(&o2)).Err) + + t.Run("o2 listed once again since now public", func(t *testing.T) { + result := <-ss.Channel().SearchInTeam(teamId, "", true) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o2}, result.Data.(*model.ChannelList)) + }) + + // o3 is a public channel on the team that already existed in the PublicChannels table. + o3 := model.Channel{ + Id: model.NewId(), + TeamId: teamId, + DisplayName: "Open Channel 3", + Name: model.NewId(), + Type: model.CHANNEL_OPEN, + } + + _, err := s.GetMaster().ExecNoTimeout(` + INSERT INTO + PublicChannels(Id, DeleteAt, TeamId, DisplayName, Name, Header, Purpose) + VALUES + (:Id, :DeleteAt, :TeamId, :DisplayName, :Name, :Header, :Purpose); + `, map[string]interface{}{ + "Id": o3.Id, + "DeleteAt": o3.DeleteAt, + "TeamId": o3.TeamId, + "DisplayName": o3.DisplayName, + "Name": o3.Name, + "Header": o3.Header, + "Purpose": o3.Purpose, + }) + require.Nil(t, err) + + o3.DisplayName = "Open Channel 3 - Modified" + + _, err = s.GetMaster().ExecNoTimeout(` + INSERT INTO + Channels(Id, CreateAt, UpdateAt, DeleteAt, TeamId, Type, DisplayName, Name, Header, Purpose, LastPostAt, TotalMsgCount, ExtraUpdateAt, CreatorId) + VALUES + (:Id, :CreateAt, :UpdateAt, :DeleteAt, :TeamId, :Type, :DisplayName, :Name, :Header, :Purpose, :LastPostAt, :TotalMsgCount, :ExtraUpdateAt, :CreatorId); + `, map[string]interface{}{ + "Id": o3.Id, + "CreateAt": o3.CreateAt, + "UpdateAt": o3.UpdateAt, + "DeleteAt": o3.DeleteAt, + "TeamId": o3.TeamId, + "Type": o3.Type, + "DisplayName": o3.DisplayName, + "Name": o3.Name, + "Header": o3.Header, + "Purpose": o3.Purpose, + "LastPostAt": o3.LastPostAt, + "TotalMsgCount": o3.TotalMsgCount, + "ExtraUpdateAt": o3.ExtraUpdateAt, + "CreatorId": o3.CreatorId, + }) + require.Nil(t, err) + + t.Run("verify o3 INSERT converted to UPDATE", func(t *testing.T) { + result := <-ss.Channel().SearchInTeam(teamId, "", true) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o2, &o3}, result.Data.(*model.ChannelList)) + }) + + // o4 is a public channel on the team that existed in the Channels table but was omitted from the PublicChannels table. + o4 := model.Channel{ + TeamId: teamId, + DisplayName: "Open Channel 4", + Name: model.NewId(), + Type: model.CHANNEL_OPEN, + } + + store.Must(ss.Channel().Save(&o4, -1)) + + _, err = s.GetMaster().ExecNoTimeout(` + DELETE FROM + PublicChannels + WHERE + Id = :Id + `, map[string]interface{}{ + "Id": o4.Id, + }) + require.Nil(t, err) + + o4.DisplayName += " - Modified" + require.Nil(t, (<-ss.Channel().Update(&o4)).Err) + + t.Run("verify o4 UPDATE converted to INSERT", func(t *testing.T) { + result := <-ss.Channel().SearchInTeam(teamId, "", true) + require.Nil(t, result.Err) + require.Equal(t, &model.ChannelList{&o2, &o3, &o4}, result.Data.(*model.ChannelList)) + }) +} diff --git a/store/storetest/command_store.go b/store/storetest/command_store.go index ffc575563..ba6c26482 100644 --- a/store/storetest/command_store.go +++ b/store/storetest/command_store.go @@ -105,7 +105,7 @@ func testCommandStoreGetByTrigger(t *testing.T, ss store.Store) { o2.Trigger = "trigger1" o1 = (<-ss.Command().Save(o1)).Data.(*model.Command) - o2 = (<-ss.Command().Save(o2)).Data.(*model.Command) + _ = (<-ss.Command().Save(o2)).Data.(*model.Command) if r1 := <-ss.Command().GetByTrigger(o1.TeamId, o1.Trigger); r1.Err != nil { t.Fatal(r1.Err) diff --git a/store/storetest/compliance_store.go b/store/storetest/compliance_store.go index f7f095a00..14ed29865 100644 --- a/store/storetest/compliance_store.go +++ b/store/storetest/compliance_store.go @@ -108,14 +108,14 @@ func testComplianceExport(t *testing.T, ss store.Store) { o1a.UserId = u1.Id o1a.CreateAt = o1.CreateAt + 10 o1a.Message = "zz" + model.NewId() + "b" - o1a = store.Must(ss.Post().Save(o1a)).(*model.Post) + _ = store.Must(ss.Post().Save(o1a)).(*model.Post) o2 := &model.Post{} o2.ChannelId = c1.Id o2.UserId = u1.Id o2.CreateAt = o1.CreateAt + 20 o2.Message = "zz" + model.NewId() + "b" - o2 = store.Must(ss.Post().Save(o2)).(*model.Post) + _ = store.Must(ss.Post().Save(o2)).(*model.Post) o2a := &model.Post{} o2a.ChannelId = c1.Id @@ -272,21 +272,21 @@ func testComplianceExportDirectMessages(t *testing.T, ss store.Store) { o1a.UserId = u1.Id o1a.CreateAt = o1.CreateAt + 10 o1a.Message = "zz" + model.NewId() + "b" - o1a = store.Must(ss.Post().Save(o1a)).(*model.Post) + _ = store.Must(ss.Post().Save(o1a)).(*model.Post) o2 := &model.Post{} o2.ChannelId = c1.Id o2.UserId = u1.Id o2.CreateAt = o1.CreateAt + 20 o2.Message = "zz" + model.NewId() + "b" - o2 = store.Must(ss.Post().Save(o2)).(*model.Post) + _ = store.Must(ss.Post().Save(o2)).(*model.Post) o2a := &model.Post{} o2a.ChannelId = c1.Id o2a.UserId = u2.Id o2a.CreateAt = o1.CreateAt + 30 o2a.Message = "zz" + model.NewId() + "b" - o2a = store.Must(ss.Post().Save(o2a)).(*model.Post) + _ = store.Must(ss.Post().Save(o2a)).(*model.Post) o3 := &model.Post{} o3.ChannelId = cDM.Id diff --git a/store/storetest/job_store.go b/store/storetest/job_store.go index 631df08fd..936999f52 100644 --- a/store/storetest/job_store.go +++ b/store/storetest/job_store.go @@ -492,7 +492,6 @@ func testJobUpdateStatusUpdateStatusOptimistically(t *testing.T, ss store.Store) if received.LastActivityAt <= lastUpdateAt { t.Fatal("lastActivityAt wasn't updated") } - lastUpdateAt = received.LastActivityAt } } diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 747a844ec..c187aae6b 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -61,6 +61,22 @@ func (_m *ChannelStore) AutocompleteInTeam(teamId string, term string, includeDe return r0 } +// AutocompleteInTeamForSearch provides a mock function with given fields: teamId, userId, term, includeDeleted +func (_m *ChannelStore) AutocompleteInTeamForSearch(teamId string, userId string, term string, includeDeleted bool) store.StoreChannel { + ret := _m.Called(teamId, userId, term, includeDeleted) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string, string, string, bool) store.StoreChannel); ok { + r0 = rf(teamId, userId, term, includeDeleted) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + // ClearAllCustomRoleAssignments provides a mock function with given fields: func (_m *ChannelStore) ClearAllCustomRoleAssignments() store.StoreChannel { ret := _m.Called() @@ -114,6 +130,30 @@ func (_m *ChannelStore) Delete(channelId string, time int64) store.StoreChannel return r0 } +// DisableExperimentalPublicChannelsMaterialization provides a mock function with given fields: +func (_m *ChannelStore) DisableExperimentalPublicChannelsMaterialization() { + _m.Called() +} + +// DropPublicChannels provides a mock function with given fields: +func (_m *ChannelStore) DropPublicChannels() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// EnableExperimentalPublicChannelsMaterialization provides a mock function with given fields: +func (_m *ChannelStore) EnableExperimentalPublicChannelsMaterialization() { + _m.Called() +} + // Get provides a mock function with given fields: id, allowFromCache func (_m *ChannelStore) Get(id string, allowFromCache bool) store.StoreChannel { ret := _m.Called(id, allowFromCache) @@ -585,6 +625,20 @@ func (_m *ChannelStore) InvalidateMemberCount(channelId string) { _m.Called(channelId) } +// IsExperimentalPublicChannelsMaterializationEnabled provides a mock function with given fields: +func (_m *ChannelStore) IsExperimentalPublicChannelsMaterializationEnabled() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + // IsUserInChannelUseCache provides a mock function with given fields: userId, channelId func (_m *ChannelStore) IsUserInChannelUseCache(userId string, channelId string) bool { ret := _m.Called(userId, channelId) @@ -615,6 +669,20 @@ func (_m *ChannelStore) MigrateChannelMembers(fromChannelId string, fromUserId s return r0 } +// MigratePublicChannels provides a mock function with given fields: +func (_m *ChannelStore) MigratePublicChannels() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + // PermanentDelete provides a mock function with given fields: channelId func (_m *ChannelStore) PermanentDelete(channelId string) store.StoreChannel { ret := _m.Called(channelId) diff --git a/store/storetest/mocks/SqlStore.go b/store/storetest/mocks/SqlStore.go index a93db78c9..38cdc0a1b 100644 --- a/store/storetest/mocks/SqlStore.go +++ b/store/storetest/mocks/SqlStore.go @@ -241,6 +241,20 @@ func (_m *SqlStore) DoesTableExist(tablename string) bool { return r0 } +// DoesTriggerExist provides a mock function with given fields: triggerName +func (_m *SqlStore) DoesTriggerExist(triggerName string) bool { + ret := _m.Called(triggerName) + + var r0 bool + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(triggerName) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + // DriverName provides a mock function with given fields: func (_m *SqlStore) DriverName() string { ret := _m.Called() diff --git a/store/storetest/mocks/SqlSupplier.go b/store/storetest/mocks/SqlSupplier.go new file mode 100644 index 000000000..4a844524d --- /dev/null +++ b/store/storetest/mocks/SqlSupplier.go @@ -0,0 +1,29 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +// Regenerate this file using `make store-mocks`. + +package mocks + +import gorp "github.com/mattermost/gorp" +import mock "github.com/stretchr/testify/mock" + +// SqlSupplier is an autogenerated mock type for the SqlSupplier type +type SqlSupplier struct { + mock.Mock +} + +// GetMaster provides a mock function with given fields: +func (_m *SqlSupplier) GetMaster() *gorp.DbMap { + ret := _m.Called() + + var r0 *gorp.DbMap + if rf, ok := ret.Get(0).(func() *gorp.DbMap); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorp.DbMap) + } + } + + return r0 +} diff --git a/store/storetest/post_store.go b/store/storetest/post_store.go index 235d6f9b7..72819f49e 100644 --- a/store/storetest/post_store.go +++ b/store/storetest/post_store.go @@ -514,7 +514,7 @@ func testPostStoreGetPostsWithDetails(t *testing.T, ss store.Store) { o2.Message = "zz" + model.NewId() + "b" o2.ParentId = o1.Id o2.RootId = o1.Id - o2 = (<-ss.Post().Save(o2)).Data.(*model.Post) + _ = (<-ss.Post().Save(o2)).Data.(*model.Post) time.Sleep(2 * time.Millisecond) o2a := &model.Post{} @@ -609,7 +609,7 @@ func testPostStoreGetPostsWithDetails(t *testing.T, ss store.Store) { o6.ChannelId = o1.ChannelId o6.UserId = model.NewId() o6.Message = "zz" + model.NewId() + "b" - o6 = (<-ss.Post().Save(o6)).Data.(*model.Post) + _ = (<-ss.Post().Save(o6)).Data.(*model.Post) // Should only be 6 since we hit the cache r3 := (<-ss.Post().GetPosts(o1.ChannelId, 0, 30, true)).Data.(*model.PostList) @@ -627,7 +627,7 @@ func testPostStoreGetPostsBeforeAfter(t *testing.T, ss store.Store) { o0.ChannelId = model.NewId() o0.UserId = model.NewId() o0.Message = "zz" + model.NewId() + "b" - o0 = (<-ss.Post().Save(o0)).Data.(*model.Post) + _ = (<-ss.Post().Save(o0)).Data.(*model.Post) time.Sleep(2 * time.Millisecond) o1 := &model.Post{} @@ -677,7 +677,7 @@ func testPostStoreGetPostsBeforeAfter(t *testing.T, ss store.Store) { o5.Message = "zz" + model.NewId() + "b" o5.ParentId = o4.Id o5.RootId = o4.Id - o5 = (<-ss.Post().Save(o5)).Data.(*model.Post) + _ = (<-ss.Post().Save(o5)).Data.(*model.Post) r1 := (<-ss.Post().GetPostsBefore(o1.ChannelId, o1.Id, 4, 0)).Data.(*model.PostList) @@ -731,7 +731,7 @@ func testPostStoreGetPostsSince(t *testing.T, ss store.Store) { o0.ChannelId = model.NewId() o0.UserId = model.NewId() o0.Message = "zz" + model.NewId() + "b" - o0 = (<-ss.Post().Save(o0)).Data.(*model.Post) + _ = (<-ss.Post().Save(o0)).Data.(*model.Post) time.Sleep(2 * time.Millisecond) o1 := &model.Post{} @@ -747,7 +747,7 @@ func testPostStoreGetPostsSince(t *testing.T, ss store.Store) { o2.Message = "zz" + model.NewId() + "b" o2.ParentId = o1.Id o2.RootId = o1.Id - o2 = (<-ss.Post().Save(o2)).Data.(*model.Post) + _ = (<-ss.Post().Save(o2)).Data.(*model.Post) time.Sleep(2 * time.Millisecond) o2a := &model.Post{} @@ -865,7 +865,7 @@ func testPostStoreSearch(t *testing.T, ss store.Store) { o1a.UserId = model.NewId() o1a.Message = "corey mattermost new york" o1a.Type = model.POST_JOIN_CHANNEL - o1a = (<-ss.Post().Save(o1a)).Data.(*model.Post) + _ = (<-ss.Post().Save(o1a)).Data.(*model.Post) o2 := &model.Post{} o2.ChannelId = c1.Id @@ -877,7 +877,7 @@ func testPostStoreSearch(t *testing.T, ss store.Store) { o3.ChannelId = c2.Id o3.UserId = model.NewId() o3.Message = "New Jersey is where John is from corey new york" - o3 = (<-ss.Post().Save(o3)).Data.(*model.Post) + _ = (<-ss.Post().Save(o3)).Data.(*model.Post) o4 := &model.Post{} o4.ChannelId = c1.Id @@ -1045,7 +1045,7 @@ func testUserCountsWithPostsByDay(t *testing.T, ss store.Store) { o1a.UserId = model.NewId() o1a.CreateAt = o1.CreateAt o1a.Message = "zz" + model.NewId() + "b" - o1a = store.Must(ss.Post().Save(o1a)).(*model.Post) + _ = store.Must(ss.Post().Save(o1a)).(*model.Post) o2 := &model.Post{} o2.ChannelId = c1.Id @@ -1059,7 +1059,7 @@ func testUserCountsWithPostsByDay(t *testing.T, ss store.Store) { o2a.UserId = o2.UserId o2a.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24) o2a.Message = "zz" + model.NewId() + "b" - o2a = store.Must(ss.Post().Save(o2a)).(*model.Post) + _ = store.Must(ss.Post().Save(o2a)).(*model.Post) if r1 := <-ss.Post().AnalyticsUserCountsWithPostsByDay(t1.Id); r1.Err != nil { t.Fatal(r1.Err) @@ -1103,7 +1103,7 @@ func testPostCountsByDay(t *testing.T, ss store.Store) { o1a.UserId = model.NewId() o1a.CreateAt = o1.CreateAt o1a.Message = "zz" + model.NewId() + "b" - o1a = store.Must(ss.Post().Save(o1a)).(*model.Post) + _ = store.Must(ss.Post().Save(o1a)).(*model.Post) o2 := &model.Post{} o2.ChannelId = c1.Id @@ -1117,7 +1117,7 @@ func testPostCountsByDay(t *testing.T, ss store.Store) { o2a.UserId = o2.UserId o2a.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24 * 2) o2a.Message = "zz" + model.NewId() + "b" - o2a = store.Must(ss.Post().Save(o2a)).(*model.Post) + _ = store.Must(ss.Post().Save(o2a)).(*model.Post) time.Sleep(1 * time.Second) @@ -1534,14 +1534,14 @@ func testPostStoreGetPostsCreatedAt(t *testing.T, ss store.Store) { o2.ParentId = o1.Id o2.RootId = o1.Id o2.CreateAt = createTime + 1 - o2 = (<-ss.Post().Save(o2)).Data.(*model.Post) + _ = (<-ss.Post().Save(o2)).Data.(*model.Post) o3 := &model.Post{} o3.ChannelId = model.NewId() o3.UserId = model.NewId() o3.Message = "zz" + model.NewId() + "b" o3.CreateAt = createTime - o3 = (<-ss.Post().Save(o3)).Data.(*model.Post) + _ = (<-ss.Post().Save(o3)).Data.(*model.Post) r1 := (<-ss.Post().GetPostsCreatedAt(o1.ChannelId, createTime)).Data.([]*model.Post) assert.Equal(t, 2, len(r1)) diff --git a/store/storetest/scheme_store.go b/store/storetest/scheme_store.go index f855ae5d4..a9204fbe2 100644 --- a/store/storetest/scheme_store.go +++ b/store/storetest/scheme_store.go @@ -20,6 +20,7 @@ func TestSchemeStore(t *testing.T, ss store.Store) { t.Run("GetAllPage", func(t *testing.T) { testSchemeStoreGetAllPage(t, ss) }) t.Run("Delete", func(t *testing.T) { testSchemeStoreDelete(t, ss) }) t.Run("PermanentDeleteAll", func(t *testing.T) { testSchemeStorePermanentDeleteAll(t, ss) }) + t.Run("GetByName", func(t *testing.T) { testSchemeStoreGetByName(t, ss) }) } func createDefaultRoles(t *testing.T, ss store.Store) { diff --git a/store/storetest/team_store.go b/store/storetest/team_store.go index ede1a91d3..1369dc69b 100644 --- a/store/storetest/team_store.go +++ b/store/storetest/team_store.go @@ -1088,9 +1088,9 @@ func testGetTeamsByScheme(t *testing.T, ss store.Store) { Type: model.TEAM_OPEN, } - t1 = (<-ss.Team().Save(t1)).Data.(*model.Team) - t2 = (<-ss.Team().Save(t2)).Data.(*model.Team) - t3 = (<-ss.Team().Save(t3)).Data.(*model.Team) + _ = (<-ss.Team().Save(t1)).Data.(*model.Team) + _ = (<-ss.Team().Save(t2)).Data.(*model.Team) + _ = (<-ss.Team().Save(t3)).Data.(*model.Team) // Get the teams by a valid Scheme ID. res1 := <-ss.Team().GetTeamsByScheme(s1.Id, 0, 100) @@ -1286,7 +1286,7 @@ func testTeamStoreAnalyticsGetTeamCountForScheme(t *testing.T, ss store.Store) { Type: model.TEAM_OPEN, SchemeId: &s1.Id, } - t1 = (<-ss.Team().Save(t1)).Data.(*model.Team) + _ = (<-ss.Team().Save(t1)).Data.(*model.Team) count2 := (<-ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)).Data.(int64) assert.Equal(t, int64(1), count2) @@ -1298,7 +1298,7 @@ func testTeamStoreAnalyticsGetTeamCountForScheme(t *testing.T, ss store.Store) { Type: model.TEAM_OPEN, SchemeId: &s1.Id, } - t2 = (<-ss.Team().Save(t2)).Data.(*model.Team) + _ = (<-ss.Team().Save(t2)).Data.(*model.Team) count3 := (<-ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)).Data.(int64) assert.Equal(t, int64(2), count3) @@ -1309,7 +1309,7 @@ func testTeamStoreAnalyticsGetTeamCountForScheme(t *testing.T, ss store.Store) { Email: MakeEmail(), Type: model.TEAM_OPEN, } - t3 = (<-ss.Team().Save(t3)).Data.(*model.Team) + _ = (<-ss.Team().Save(t3)).Data.(*model.Team) count4 := (<-ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)).Data.(int64) assert.Equal(t, int64(2), count4) @@ -1322,7 +1322,7 @@ func testTeamStoreAnalyticsGetTeamCountForScheme(t *testing.T, ss store.Store) { SchemeId: &s1.Id, DeleteAt: model.GetMillis(), } - t4 = (<-ss.Team().Save(t4)).Data.(*model.Team) + _ = (<-ss.Team().Save(t4)).Data.(*model.Team) count5 := (<-ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)).Data.(int64) assert.Equal(t, int64(2), count5) diff --git a/store/storetest/webhook_store.go b/store/storetest/webhook_store.go index 2dfa2ae53..2b30f2d33 100644 --- a/store/storetest/webhook_store.go +++ b/store/storetest/webhook_store.go @@ -484,7 +484,7 @@ func testWebhookStoreCountIncoming(t *testing.T, ss store.Store) { o1.UserId = model.NewId() o1.TeamId = model.NewId() - o1 = (<-ss.Webhook().SaveIncoming(o1)).Data.(*model.IncomingWebhook) + _ = (<-ss.Webhook().SaveIncoming(o1)).Data.(*model.IncomingWebhook) if r := <-ss.Webhook().AnalyticsIncomingCount(""); r.Err != nil { t.Fatal(r.Err) @@ -502,7 +502,7 @@ func testWebhookStoreCountOutgoing(t *testing.T, ss store.Store) { o1.TeamId = model.NewId() o1.CallbackURLs = []string{"http://nowhere.com/"} - o1 = (<-ss.Webhook().SaveOutgoing(o1)).Data.(*model.OutgoingWebhook) + _ = (<-ss.Webhook().SaveOutgoing(o1)).Data.(*model.OutgoingWebhook) if r := <-ss.Webhook().AnalyticsOutgoingCount(""); r.Err != nil { t.Fatal(r.Err) diff --git a/utils/config.go b/utils/config.go index 7e5a42faa..786e248ca 100644 --- a/utils/config.go +++ b/utils/config.go @@ -51,9 +51,7 @@ func FindPath(path string, baseSearchPaths []string, filter func(os.FileInfo) bo } searchPaths := []string{} - for _, baseSearchPath := range baseSearchPaths { - searchPaths = append(searchPaths, baseSearchPath) - } + searchPaths = append(searchPaths, baseSearchPaths...) // Additionally attempt to search relative to the location of the running binary. var binaryDir string diff --git a/utils/i18n.go b/utils/i18n.go index d7c55e4e6..4fcd7669d 100644 --- a/utils/i18n.go +++ b/utils/i18n.go @@ -27,12 +27,7 @@ func TranslationsPreInit() error { // segfault trying to handle the error, and the untranslated IDs are strictly better. T = TfuncWithFallback("en") TDefault = TfuncWithFallback("en") - - if err := InitTranslationsWithDir("i18n"); err != nil { - return err - } - - return nil + return InitTranslationsWithDir("i18n") } func InitTranslations(localizationSettings model.LocalizationSettings) error { diff --git a/utils/jsonutils/json_test.go b/utils/jsonutils/json_test.go index b3986e87b..85cb66d60 100644 --- a/utils/jsonutils/json_test.go +++ b/utils/jsonutils/json_test.go @@ -76,8 +76,6 @@ func TestHumanizeJsonError(t *testing.T) { func TestNewHumanizedJsonError(t *testing.T) { t.Parallel() - type testType struct{} - testCases := []struct { Description string Data []byte diff --git a/utils/mail.go b/utils/mail.go index 7b0cb3588..750cb64fe 100644 --- a/utils/mail.go +++ b/utils/mail.go @@ -257,20 +257,18 @@ func SendMail(c *smtp.Client, mimeTo, smtpTo string, from mail.Address, subject, m.SetBody("text/plain", txtBody) m.AddAlternative("text/html", htmlMessage) - if attachments != nil { - for _, fileInfo := range attachments { - bytes, err := fileBackend.ReadFile(fileInfo.Path) - if err != nil { - return err - } - - m.Attach(fileInfo.Name, gomail.SetCopyFunc(func(writer io.Writer) error { - if _, err := writer.Write(bytes); err != nil { - return model.NewAppError("SendMail", "utils.mail.sendMail.attachments.write_error", nil, err.Error(), http.StatusInternalServerError) - } - return nil - })) + for _, fileInfo := range attachments { + bytes, err := fileBackend.ReadFile(fileInfo.Path) + if err != nil { + return err } + + m.Attach(fileInfo.Name, gomail.SetCopyFunc(func(writer io.Writer) error { + if _, err := writer.Write(bytes); err != nil { + return model.NewAppError("SendMail", "utils.mail.sendMail.attachments.write_error", nil, err.Error(), http.StatusInternalServerError) + } + return nil + })) } if err := c.Mail(from.Address); err != nil { |