summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile8
-rw-r--r--api4/channel.go25
-rw-r--r--api4/channel_test.go21
-rw-r--r--api4/command_test.go3
-rw-r--r--api4/emoji_test.go17
-rw-r--r--api4/file_test.go5
-rw-r--r--api4/oauth.go9
-rw-r--r--api4/oauth_test.go26
-rw-r--r--api4/post_test.go29
-rw-r--r--api4/preference_test.go12
-rw-r--r--api4/role_test.go16
-rw-r--r--api4/status_test.go4
-rw-r--r--api4/team_test.go26
-rw-r--r--api4/user_test.go18
-rw-r--r--api4/webhook_test.go2
-rw-r--r--app/app.go63
-rw-r--r--app/apptestlib.go30
-rw-r--r--app/auto_responder_test.go4
-rw-r--r--app/brand.go2
-rw-r--r--app/channel.go14
-rw-r--r--app/command.go8
-rw-r--r--app/command_channel_header.go39
-rw-r--r--app/command_channel_header_test.go6
-rw-r--r--app/command_channel_purpose.go48
-rw-r--r--app/command_channel_purpose_test.go93
-rw-r--r--app/command_channel_rename.go57
-rw-r--r--app/command_channel_rename_test.go65
-rw-r--r--app/command_invite.go103
-rw-r--r--app/command_invite_test.go7
-rw-r--r--app/command_join.go53
-rw-r--r--app/command_join_test.go106
-rw-r--r--app/command_remove.go19
-rw-r--r--app/command_remove_test.go109
-rw-r--r--app/config_test.go2
-rw-r--r--app/diagnostics_test.go10
-rw-r--r--app/file.go9
-rw-r--r--app/http_service.go67
-rw-r--r--app/http_service_test.go65
-rw-r--r--app/import_functions.go4
-rw-r--r--app/notification_email.go2
-rw-r--r--app/notification_push.go2
-rw-r--r--app/oauth.go4
-rw-r--r--app/permissions.go6
-rw-r--r--app/plugin_hooks_test.go90
-rw-r--r--app/post.go6
-rw-r--r--app/session_test.go2
-rw-r--r--app/team.go4
-rw-r--r--app/user.go6
-rw-r--r--app/webhook.go2
-rw-r--r--app/webrtc.go4
-rw-r--r--cmd/mattermost/commands/channel.go39
-rw-r--r--cmd/mattermost/commands/permissions.go6
-rw-r--r--cmd/mattermost/commands/permissions_test.go2
-rw-r--r--config/default.json3
-rw-r--r--i18n/en.json398
-rw-r--r--model/client4.go11
-rw-r--r--model/config.go27
-rw-r--r--model/incoming_webhook.go2
-rw-r--r--model/incoming_webhook_test.go4
-rw-r--r--model/outgoing_webhook.go2
-rw-r--r--model/outgoing_webhook_test.go6
-rw-r--r--model/session_test.go2
-rw-r--r--model/user_test.go2
-rw-r--r--plugin/client_rpc.go34
-rw-r--r--plugin/client_rpc_generated.go194
-rw-r--r--plugin/environment.go1
-rw-r--r--plugin/interface_generator/main.go49
-rw-r--r--plugin/io_rpc.go13
-rw-r--r--plugin/supervisor.go1
-rw-r--r--store/sqlstore/channel_store.go87
-rw-r--r--store/sqlstore/channel_store_experimental.go819
-rw-r--r--store/sqlstore/channel_store_test.go2
-rw-r--r--store/sqlstore/store.go1
-rw-r--r--store/sqlstore/store_test.go25
-rw-r--r--store/sqlstore/supplier.go65
-rw-r--r--store/sqlstore/team_store.go2
-rw-r--r--store/sqlstore/upgrade.go16
-rw-r--r--store/sqlstore/user_store.go2
-rw-r--r--store/sqlstore/webhook_store.go4
-rw-r--r--store/store.go6
-rw-r--r--store/storetest/channel_store.go1403
-rw-r--r--store/storetest/command_store.go2
-rw-r--r--store/storetest/compliance_store.go10
-rw-r--r--store/storetest/job_store.go1
-rw-r--r--store/storetest/mocks/ChannelStore.go68
-rw-r--r--store/storetest/mocks/SqlStore.go14
-rw-r--r--store/storetest/mocks/SqlSupplier.go29
-rw-r--r--store/storetest/post_store.go28
-rw-r--r--store/storetest/scheme_store.go1
-rw-r--r--store/storetest/team_store.go14
-rw-r--r--store/storetest/webhook_store.go4
-rw-r--r--utils/config.go4
-rw-r--r--utils/i18n.go7
-rw-r--r--utils/jsonutils/json_test.go2
-rw-r--r--utils/mail.go24
95 files changed, 3452 insertions, 1316 deletions
diff --git a/Makefile b/Makefile
index 94adc5fb8..6ac85a785 100644
--- a/Makefile
+++ b/Makefile
@@ -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 {