summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api4/status.go5
-rw-r--r--api4/system.go13
-rw-r--r--api4/system_test.go64
-rw-r--r--api4/user.go11
-rw-r--r--api4/user_test.go3
-rw-r--r--app/admin.go6
-rw-r--r--app/app.go1
-rw-r--r--app/auto_responder.go70
-rw-r--r--app/auto_responder_test.go160
-rw-r--r--app/channel.go20
-rw-r--r--app/channel_test.go41
-rw-r--r--app/command_mute.go5
-rw-r--r--app/command_mute_test.go204
-rw-r--r--app/config.go10
-rw-r--r--app/notification.go24
-rw-r--r--app/notification_test.go24
-rw-r--r--app/plugin_api.go25
-rw-r--r--app/status.go26
-rw-r--r--cmd/commands/channel.go4
-rw-r--r--cmd/commands/config_flag_test.go2
-rw-r--r--cmd/commands/message_export_test.go2
-rw-r--r--cmd/commands/team.go27
-rw-r--r--cmd/commands/team_test.go18
-rw-r--r--config/default.json1
-rw-r--r--glide.lock2
-rw-r--r--glide.yaml2
-rw-r--r--i18n/en.json20
-rw-r--r--model/client4.go12
-rw-r--r--model/config.go7
-rw-r--r--model/config_test.go46
-rw-r--r--model/post.go2
-rw-r--r--model/status.go1
-rw-r--r--model/utils.go59
-rw-r--r--model/utils_test.go183
-rw-r--r--model/websocket_client.go22
-rw-r--r--plugin/api.go12
-rw-r--r--plugin/plugintest/api.go39
-rw-r--r--plugin/rpcplugin/api.go103
-rw-r--r--plugin/rpcplugin/api_test.go23
-rw-r--r--store/sqlstore/upgrade.go10
-rw-r--r--utils/config.go178
-rw-r--r--utils/config_test.go162
-rw-r--r--utils/mail_test.go6
-rw-r--r--vendor/github.com/spf13/viper/nohup.out1
-rw-r--r--vendor/github.com/spf13/viper/viper.go38
-rw-r--r--web/web_test.go5
46 files changed, 1586 insertions, 113 deletions
diff --git a/api4/status.go b/api4/status.go
index 59909e295..627ddaca6 100644
--- a/api4/status.go
+++ b/api4/status.go
@@ -71,6 +71,11 @@ func updateUserStatus(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
+ currentStatus, err := c.App.GetStatus(c.Params.UserId)
+ if err == nil && currentStatus.Status == model.STATUS_OUT_OF_OFFICE && status.Status != model.STATUS_OUT_OF_OFFICE {
+ c.App.DisableAutoResponder(c.Params.UserId, c.IsSystemAdmin())
+ }
+
switch status.Status {
case "online":
c.App.SetStatusOnline(c.Params.UserId, "", true)
diff --git a/api4/system.go b/api4/system.go
index b34f2af6b..c307a39b7 100644
--- a/api4/system.go
+++ b/api4/system.go
@@ -23,6 +23,7 @@ func (api *API) InitSystem() {
api.BaseRoutes.ApiRoot.Handle("/config", api.ApiSessionRequired(updateConfig)).Methods("PUT")
api.BaseRoutes.ApiRoot.Handle("/config/reload", api.ApiSessionRequired(configReload)).Methods("POST")
api.BaseRoutes.ApiRoot.Handle("/config/client", api.ApiHandler(getClientConfig)).Methods("GET")
+ api.BaseRoutes.ApiRoot.Handle("/config/environment", api.ApiSessionRequired(getEnvironmentConfig)).Methods("GET")
api.BaseRoutes.ApiRoot.Handle("/license", api.ApiSessionRequired(addLicense)).Methods("POST")
api.BaseRoutes.ApiRoot.Handle("/license", api.ApiSessionRequired(removeLicense)).Methods("DELETE")
@@ -251,6 +252,18 @@ func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(c.App.ClientConfigWithComputed())))
}
+func getEnvironmentConfig(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+
+ envConfig := c.App.GetEnvironmentConfig()
+
+ w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
+ w.Write([]byte(model.StringInterfaceToJson(envConfig)))
+}
+
func getClientLicense(c *Context, w http.ResponseWriter, r *http.Request) {
format := r.URL.Query().Get("format")
diff --git a/api4/system_test.go b/api4/system_test.go
index bb3790d4b..f74d91563 100644
--- a/api4/system_test.go
+++ b/api4/system_test.go
@@ -161,6 +161,70 @@ func TestUpdateConfig(t *testing.T) {
})
}
+func TestGetEnvironmentConfig(t *testing.T) {
+ os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://example.mattermost.com")
+ os.Setenv("MM_SERVICESETTINGS_ENABLECUSTOMEMOJI", "true")
+ defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
+
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer th.TearDown()
+
+ t.Run("as system admin", func(t *testing.T) {
+ SystemAdminClient := th.SystemAdminClient
+
+ envConfig, resp := SystemAdminClient.GetEnvironmentConfig()
+ CheckNoError(t, resp)
+
+ if serviceSettings, ok := envConfig["ServiceSettings"]; !ok {
+ t.Fatal("should've returned ServiceSettings")
+ } else if serviceSettingsAsMap, ok := serviceSettings.(map[string]interface{}); !ok {
+ t.Fatal("should've returned ServiceSettings as a map")
+ } else {
+ if siteURL, ok := serviceSettingsAsMap["SiteURL"]; !ok {
+ t.Fatal("should've returned ServiceSettings.SiteURL")
+ } else if siteURLAsBool, ok := siteURL.(bool); !ok {
+ t.Fatal("should've returned ServiceSettings.SiteURL as a boolean")
+ } else if !siteURLAsBool {
+ t.Fatal("should've returned ServiceSettings.SiteURL as true")
+ }
+
+ if enableCustomEmoji, ok := serviceSettingsAsMap["EnableCustomEmoji"]; !ok {
+ t.Fatal("should've returned ServiceSettings.EnableCustomEmoji")
+ } else if enableCustomEmojiAsBool, ok := enableCustomEmoji.(bool); !ok {
+ t.Fatal("should've returned ServiceSettings.EnableCustomEmoji as a boolean")
+ } else if !enableCustomEmojiAsBool {
+ t.Fatal("should've returned ServiceSettings.EnableCustomEmoji as true")
+ }
+ }
+
+ if _, ok := envConfig["TeamSettings"]; ok {
+ t.Fatal("should not have returned TeamSettings")
+ }
+ })
+
+ t.Run("as team admin", func(t *testing.T) {
+ TeamAdminClient := th.CreateClient()
+ th.LoginTeamAdminWithClient(TeamAdminClient)
+
+ _, resp := TeamAdminClient.GetEnvironmentConfig()
+ CheckForbiddenStatus(t, resp)
+ })
+
+ t.Run("as regular user", func(t *testing.T) {
+ Client := th.Client
+
+ _, resp := Client.GetEnvironmentConfig()
+ CheckForbiddenStatus(t, resp)
+ })
+
+ t.Run("as not-regular user", func(t *testing.T) {
+ Client := th.CreateClient()
+
+ _, resp := Client.GetEnvironmentConfig()
+ CheckUnauthorizedStatus(t, resp)
+ })
+}
+
func TestGetOldClientConfig(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
defer th.TearDown()
diff --git a/api4/user.go b/api4/user.go
index 8f8f08c75..9aa709db5 100644
--- a/api4/user.go
+++ b/api4/user.go
@@ -199,7 +199,8 @@ func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
return
} else {
if len(users) == 0 {
- c.Err = err
+ c.Err = model.NewAppError("getProfileImage", "api.user.get_profile_image.not_found.app_error", nil, "", http.StatusNotFound)
+ return
}
user := users[0]
@@ -589,8 +590,13 @@ func patchUser(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
+ ouser, err := c.App.GetUser(c.Params.UserId)
+ if err != nil {
+ c.SetInvalidParam("user_id")
+ return
+ }
+
if c.Session.IsOAuth && patch.Email != nil {
- ouser, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
@@ -607,6 +613,7 @@ func patchUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.Err = err
return
} else {
+ c.App.SetAutoResponderStatus(ruser, ouser.NotifyProps)
c.LogAudit("")
w.Write([]byte(ruser.ToJson()))
}
diff --git a/api4/user_test.go b/api4/user_test.go
index 359756aeb..27219726b 100644
--- a/api4/user_test.go
+++ b/api4/user_test.go
@@ -834,6 +834,9 @@ func TestGetProfileImage(t *testing.T) {
_, resp = Client.GetProfileImage("junk", "")
CheckBadRequestStatus(t, resp)
+ _, resp = Client.GetProfileImage(model.NewId(), "")
+ CheckNotFoundStatus(t, resp)
+
Client.Logout()
_, resp = Client.GetProfileImage(user.Id, "")
CheckUnauthorizedStatus(t, resp)
diff --git a/app/admin.go b/app/admin.go
index 22928390e..60b71505a 100644
--- a/app/admin.go
+++ b/app/admin.go
@@ -156,6 +156,10 @@ func (a *App) GetConfig() *model.Config {
return cfg
}
+func (a *App) GetEnvironmentConfig() map[string]interface{} {
+ return a.EnvironmentConfig()
+}
+
func (a *App) SaveConfig(cfg *model.Config, sendConfigChangeClusterMessage bool) *model.AppError {
oldCfg := a.Config()
cfg.SetDefaults()
@@ -239,7 +243,7 @@ func (a *App) TestEmail(userId string, cfg *model.Config) *model.AppError {
T := utils.GetUserTranslations(user.Locale)
license := a.License()
if err := utils.SendMailUsingConfig(user.Email, T("api.admin.test_email.subject"), T("api.admin.test_email.body"), cfg, license != nil && *license.Features.Compliance); err != nil {
- return err
+ return model.NewAppError("testEmail", "app.admin.test_email.failure", map[string]interface{}{"Error": err.Error()}, "", http.StatusInternalServerError)
}
}
diff --git a/app/app.go b/app/app.go
index 27227d271..43f598f79 100644
--- a/app/app.go
+++ b/app/app.go
@@ -59,6 +59,7 @@ type App struct {
Saml einterfaces.SamlInterface
config atomic.Value
+ envConfig map[string]interface{}
configFile string
configListeners map[string]func(*model.Config, *model.Config)
diff --git a/app/auto_responder.go b/app/auto_responder.go
new file mode 100644
index 000000000..23402ecd4
--- /dev/null
+++ b/app/auto_responder.go
@@ -0,0 +1,70 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ l4g "github.com/alecthomas/log4go"
+
+ "github.com/mattermost/mattermost-server/model"
+)
+
+func (a *App) SendAutoResponse(channel *model.Channel, receiver *model.User, rootId string) {
+ if receiver == nil || receiver.NotifyProps == nil {
+ return
+ }
+
+ active := receiver.NotifyProps["auto_responder_active"] == "true"
+ message := receiver.NotifyProps["auto_responder_message"]
+
+ if active && message != "" {
+ autoResponderPost := &model.Post{
+ ChannelId: channel.Id,
+ Message: message,
+ RootId: rootId,
+ ParentId: rootId,
+ Type: model.POST_AUTO_RESPONDER,
+ UserId: receiver.Id,
+ }
+
+ if _, err := a.CreatePost(autoResponderPost, channel, false); err != nil {
+ l4g.Error(err.Error())
+ }
+ }
+}
+
+func (a *App) SetAutoResponderStatus(user *model.User, oldNotifyProps model.StringMap) {
+ active := user.NotifyProps["auto_responder_active"] == "true"
+ oldActive := oldNotifyProps["auto_responder_active"] == "true"
+
+ autoResponderEnabled := !oldActive && active
+ autoResponderDisabled := oldActive && !active
+
+ if autoResponderEnabled {
+ a.SetStatusOutOfOffice(user.Id)
+ } else if autoResponderDisabled {
+ a.SetStatusOnline(user.Id, "", true)
+ }
+}
+
+func (a *App) DisableAutoResponder(userId string, asAdmin bool) *model.AppError {
+ user, err := a.GetUser(userId)
+ if err != nil {
+ return err
+ }
+
+ active := user.NotifyProps["auto_responder_active"] == "true"
+
+ if active {
+ patch := &model.UserPatch{}
+ patch.NotifyProps = user.NotifyProps
+ patch.NotifyProps["auto_responder_active"] = "false"
+
+ _, err := a.PatchUser(userId, patch, asAdmin)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/app/auto_responder_test.go b/app/auto_responder_test.go
new file mode 100644
index 000000000..65b466c92
--- /dev/null
+++ b/app/auto_responder_test.go
@@ -0,0 +1,160 @@
+// Copyright (c) 2016-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/stretchr/testify/require"
+)
+
+func TestSetAutoResponderStatus(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ user := th.CreateUser()
+ defer th.App.PermanentDeleteUser(user)
+
+ th.App.SetStatusOnline(user.Id, "", true)
+
+ patch := &model.UserPatch{}
+ patch.NotifyProps = make(map[string]string)
+ patch.NotifyProps["auto_responder_active"] = "true"
+ patch.NotifyProps["auto_responder_message"] = "Hello, I'm unavailable today."
+
+ userUpdated1, _ := th.App.PatchUser(user.Id, patch, true)
+
+ // autoResponder is enabled, status should be OOO
+ th.App.SetAutoResponderStatus(userUpdated1, user.NotifyProps)
+
+ status, err := th.App.GetStatus(userUpdated1.Id)
+ require.Nil(t, err)
+ assert.Equal(t, model.STATUS_OUT_OF_OFFICE, status.Status)
+
+ patch2 := &model.UserPatch{}
+ patch2.NotifyProps = make(map[string]string)
+ patch2.NotifyProps["auto_responder_active"] = "false"
+ patch2.NotifyProps["auto_responder_message"] = "Hello, I'm unavailable today."
+
+ userUpdated2, _ := th.App.PatchUser(user.Id, patch2, true)
+
+ // autoResponder is disabled, status should be ONLINE
+ th.App.SetAutoResponderStatus(userUpdated2, userUpdated1.NotifyProps)
+
+ status, err = th.App.GetStatus(userUpdated2.Id)
+ require.Nil(t, err)
+ assert.Equal(t, model.STATUS_ONLINE, status.Status)
+
+}
+
+func TestDisableAutoResponder(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ user := th.CreateUser()
+ defer th.App.PermanentDeleteUser(user)
+
+ th.App.SetStatusOnline(user.Id, "", true)
+
+ patch := &model.UserPatch{}
+ patch.NotifyProps = make(map[string]string)
+ patch.NotifyProps["auto_responder_active"] = "true"
+ patch.NotifyProps["auto_responder_message"] = "Hello, I'm unavailable today."
+
+ th.App.PatchUser(user.Id, patch, true)
+
+ th.App.DisableAutoResponder(user.Id, true)
+
+ userUpdated1, err := th.App.GetUser(user.Id)
+ require.Nil(t, err)
+ assert.Equal(t, userUpdated1.NotifyProps["auto_responder_active"], "false")
+
+ th.App.DisableAutoResponder(user.Id, true)
+
+ userUpdated2, err := th.App.GetUser(user.Id)
+ require.Nil(t, err)
+ assert.Equal(t, userUpdated2.NotifyProps["auto_responder_active"], "false")
+}
+
+func TestSendAutoResponseSuccess(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ user := th.CreateUser()
+ defer th.App.PermanentDeleteUser(user)
+
+ patch := &model.UserPatch{}
+ patch.NotifyProps = make(map[string]string)
+ patch.NotifyProps["auto_responder_active"] = "true"
+ patch.NotifyProps["auto_responder_message"] = "Hello, I'm unavailable today."
+
+ userUpdated1, err := th.App.PatchUser(user.Id, patch, true)
+ require.Nil(t, err)
+
+ firstPost, err := th.App.CreatePost(&model.Post{
+ ChannelId: th.BasicChannel.Id,
+ Message: "zz" + model.NewId() + "a",
+ UserId: th.BasicUser.Id},
+ th.BasicChannel,
+ false)
+
+ th.App.SendAutoResponse(th.BasicChannel, userUpdated1, firstPost.Id)
+
+ if list, err := th.App.GetPosts(th.BasicChannel.Id, 0, 1); err != nil {
+ require.Nil(t, err)
+ } else {
+ autoResponderPostFound := false
+ autoResponderIsComment := false
+ for _, post := range list.Posts {
+ if post.Type == model.POST_AUTO_RESPONDER {
+ autoResponderIsComment = post.RootId == firstPost.Id
+ autoResponderPostFound = true
+ }
+ }
+ assert.True(t, autoResponderPostFound)
+ assert.True(t, autoResponderIsComment)
+ }
+}
+
+func TestSendAutoResponseFailure(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ user := th.CreateUser()
+ defer th.App.PermanentDeleteUser(user)
+
+ patch := &model.UserPatch{}
+ patch.NotifyProps = make(map[string]string)
+ patch.NotifyProps["auto_responder_active"] = "false"
+ patch.NotifyProps["auto_responder_message"] = "Hello, I'm unavailable today."
+
+ userUpdated1, err := th.App.PatchUser(user.Id, patch, true)
+ require.Nil(t, err)
+
+ firstPost, err := th.App.CreatePost(&model.Post{
+ ChannelId: th.BasicChannel.Id,
+ Message: "zz" + model.NewId() + "a",
+ UserId: th.BasicUser.Id},
+ th.BasicChannel,
+ false)
+
+ th.App.SendAutoResponse(th.BasicChannel, userUpdated1, firstPost.Id)
+
+ if list, err := th.App.GetPosts(th.BasicChannel.Id, 0, 1); err != nil {
+ require.Nil(t, err)
+ } else {
+ autoResponderPostFound := false
+ autoResponderIsComment := false
+ for _, post := range list.Posts {
+ if post.Type == model.POST_AUTO_RESPONDER {
+ autoResponderIsComment = post.RootId == firstPost.Id
+ autoResponderPostFound = true
+ }
+ }
+ assert.False(t, autoResponderPostFound)
+ assert.False(t, autoResponderIsComment)
+ }
+}
diff --git a/app/channel.go b/app/channel.go
index 6e11d4e5d..7c14538f4 100644
--- a/app/channel.go
+++ b/app/channel.go
@@ -652,8 +652,10 @@ func (a *App) AddChannelMember(userId string, channel *model.Channel, userReques
}
var userRequestor *model.User
- if userRequestor, err = a.GetUser(userRequestorId); err != nil {
- return nil, err
+ if userRequestorId != "" {
+ if userRequestor, err = a.GetUser(userRequestorId); err != nil {
+ return nil, err
+ }
}
cm, err := a.AddUserToChannel(user, channel)
@@ -661,7 +663,7 @@ func (a *App) AddChannelMember(userId string, channel *model.Channel, userReques
return nil, err
}
- if userId == userRequestorId {
+ if userRequestorId == "" || userId == userRequestorId {
a.postJoinChannelMessage(user, channel)
} else {
a.Go(func() {
@@ -669,7 +671,9 @@ func (a *App) AddChannelMember(userId string, channel *model.Channel, userReques
})
}
- a.UpdateChannelLastViewedAt([]string{channel.Id}, userRequestor.Id)
+ if userRequestor != nil {
+ a.UpdateChannelLastViewedAt([]string{channel.Id}, userRequestor.Id)
+ }
return cm, nil
}
@@ -1487,7 +1491,13 @@ func (a *App) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.
}
func (a *App) ToggleMuteChannel(channelId string, userId string) *model.ChannelMember {
- member := (<-a.Srv.Store.Channel().GetMember(channelId, userId)).Data.(*model.ChannelMember)
+ result := <-a.Srv.Store.Channel().GetMember(channelId, userId)
+
+ if result.Err != nil {
+ return nil
+ }
+
+ member := result.Data.(*model.ChannelMember)
if member.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP] == model.CHANNEL_NOTIFY_MENTION {
member.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP] = model.CHANNEL_MARK_UNREAD_ALL
diff --git a/app/channel_test.go b/app/channel_test.go
index 69efaeca7..a4e0806a6 100644
--- a/app/channel_test.go
+++ b/app/channel_test.go
@@ -340,3 +340,44 @@ func TestRemoveUserFromChannelUpdatesChannelMemberHistoryRecord(t *testing.T) {
assert.Equal(t, publicChannel.Id, histories[0].ChannelId)
assert.NotNil(t, histories[0].LeaveTime)
}
+
+func TestAddChannelMemberNoUserRequestor(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ // create a user and add it to a channel
+ user := th.CreateUser()
+ if _, err := th.App.AddTeamMember(th.BasicTeam.Id, user.Id); err != nil {
+ t.Fatal("Failed to add user to team. Error: " + err.Message)
+ }
+
+ groupUserIds := make([]string, 0)
+ groupUserIds = append(groupUserIds, th.BasicUser.Id)
+ groupUserIds = append(groupUserIds, user.Id)
+
+ channel := th.createChannel(th.BasicTeam, model.CHANNEL_OPEN)
+ userRequestorId := ""
+ postRootId := ""
+ if _, err := th.App.AddChannelMember(user.Id, channel, userRequestorId, postRootId); err != nil {
+ t.Fatal("Failed to add user to channel. Error: " + err.Message)
+ }
+
+ // there should be a ChannelMemberHistory record for the user
+ histories := store.Must(th.App.Srv.Store.ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, channel.Id)).([]*model.ChannelMemberHistoryResult)
+ assert.Len(t, histories, 2)
+ channelMemberHistoryUserIds := make([]string, 0)
+ for _, history := range histories {
+ assert.Equal(t, channel.Id, history.ChannelId)
+ channelMemberHistoryUserIds = append(channelMemberHistoryUserIds, history.UserId)
+ }
+ assert.Equal(t, groupUserIds, channelMemberHistoryUserIds)
+
+ postList := store.Must(th.App.Srv.Store.Post().GetPosts(channel.Id, 0, 1, false)).(*model.PostList)
+ if assert.Len(t, postList.Order, 1) {
+ post := postList.Posts[postList.Order[0]]
+
+ assert.Equal(t, model.POST_JOIN_CHANNEL, post.Type)
+ assert.Equal(t, user.Id, post.UserId)
+ assert.Equal(t, user.Username, post.Props["username"])
+ }
+}
diff --git a/app/command_mute.go b/app/command_mute.go
index fdc698cd9..d3a2fa5f7 100644
--- a/app/command_mute.go
+++ b/app/command_mute.go
@@ -40,7 +40,7 @@ func (me *MuteProvider) DoCommand(a *App, args *model.CommandArgs, message strin
var noChannelErr *model.AppError
if channel, noChannelErr = a.GetChannel(args.ChannelId); noChannelErr != nil {
- return &model.CommandResponse{Text: args.T("api.command_mute.error", map[string]interface{}{"Channel": channel.DisplayName}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ return &model.CommandResponse{Text: args.T("api.command_mute.no_channel.error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
}
channelName := ""
@@ -63,6 +63,9 @@ func (me *MuteProvider) DoCommand(a *App, args *model.CommandArgs, message strin
}
channelMember := a.ToggleMuteChannel(channel.Id, args.UserId)
+ if channelMember == nil {
+ return &model.CommandResponse{Text: args.T("api.command_mute.not_member.error", map[string]interface{}{"Channel": channelName}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
// Invalidate cache to allow cache lookups while sending notifications
a.Srv.Store.Channel().InvalidateCacheForChannelMembersNotifyProps(channel.Id)
diff --git a/app/command_mute_test.go b/app/command_mute_test.go
new file mode 100644
index 000000000..9f82c48cc
--- /dev/null
+++ b/app/command_mute_test.go
@@ -0,0 +1,204 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "github.com/mattermost/mattermost-server/model"
+ "github.com/nicksnyder/go-i18n/i18n"
+ "github.com/stretchr/testify/assert"
+ "testing"
+ "time"
+)
+
+func TestMuteCommandNoChannel(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ if testing.Short() {
+ t.SkipNow()
+ }
+
+ channel1 := th.BasicChannel
+ channel1M, channel1MError := th.App.GetChannelMember(channel1.Id, th.BasicUser.Id)
+
+ if channel1MError != nil {
+ t.Fatal("User is not a member of channel 1")
+ }
+
+ if channel1M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP] == model.CHANNEL_NOTIFY_MENTION {
+ t.Fatal("channel shouldn't be muted on initial setup")
+ }
+
+ cmd := &MuteProvider{}
+ resp := cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ UserId: th.BasicUser.Id,
+ }, "")
+ assert.Equal(t, "api.command_mute.no_channel.error", resp.Text)
+}
+
+func TestMuteCommandNoArgs(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ channel1 := th.BasicChannel
+ channel1M, _ := th.App.GetChannelMember(channel1.Id, th.BasicUser.Id)
+
+ assert.Equal(t, model.CHANNEL_NOTIFY_ALL, channel1M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP])
+
+ cmd := &MuteProvider{}
+
+ // First mute the channel
+ resp := cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ ChannelId: channel1.Id,
+ UserId: th.BasicUser.Id,
+ }, "")
+ assert.Equal(t, "api.command_mute.success_mute", resp.Text)
+
+ // Now unmute the channel
+ time.Sleep(time.Millisecond)
+ resp = cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ ChannelId: channel1.Id,
+ UserId: th.BasicUser.Id,
+ }, "")
+
+ assert.Equal(t, "api.command_mute.success_unmute", resp.Text)
+}
+
+func TestMuteCommandSpecificChannel(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ if testing.Short() {
+ t.SkipNow()
+ }
+
+ channel1 := th.BasicChannel
+ 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,
+ }, true)
+
+ channel2M, _ := th.App.GetChannelMember(channel2.Id, th.BasicUser.Id)
+
+ assert.Equal(t, model.CHANNEL_NOTIFY_ALL, channel2M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP])
+
+ cmd := &MuteProvider{}
+
+ // First mute the channel
+ resp := cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ ChannelId: channel1.Id,
+ UserId: th.BasicUser.Id,
+ }, channel2.Name)
+ assert.Equal(t, "api.command_mute.success_mute", resp.Text)
+ time.Sleep(time.Millisecond)
+ channel2M, _ = th.App.GetChannelMember(channel2.Id, th.BasicUser.Id)
+ assert.Equal(t, model.CHANNEL_NOTIFY_MENTION, channel2M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP])
+
+ // Now unmute the channel
+ resp = cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ ChannelId: channel1.Id,
+ UserId: th.BasicUser.Id,
+ }, "~"+channel2.Name)
+
+ assert.Equal(t, "api.command_mute.success_unmute", resp.Text)
+ time.Sleep(time.Millisecond)
+ channel2M, _ = th.App.GetChannelMember(channel2.Id, th.BasicUser.Id)
+ assert.Equal(t, model.CHANNEL_NOTIFY_ALL, channel2M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP])
+}
+
+func TestMuteCommandNotMember(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ if testing.Short() {
+ t.SkipNow()
+ }
+
+ channel1 := th.BasicChannel
+ 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 := &MuteProvider{}
+
+ // First mute the channel
+ resp := cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ ChannelId: channel1.Id,
+ UserId: th.BasicUser.Id,
+ }, channel2.Name)
+ assert.Equal(t, "api.command_mute.not_member.error", resp.Text)
+}
+
+func TestMuteCommandNotChannel(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ if testing.Short() {
+ t.SkipNow()
+ }
+
+ channel1 := th.BasicChannel
+
+ cmd := &MuteProvider{}
+
+ // First mute the channel
+ resp := cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ ChannelId: channel1.Id,
+ UserId: th.BasicUser.Id,
+ }, "~noexists")
+ assert.Equal(t, "api.command_mute.error", resp.Text)
+}
+
+func TestMuteCommandDMChannel(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ if testing.Short() {
+ t.SkipNow()
+ }
+
+ channel2, _ := th.App.CreateDirectChannel(th.BasicUser.Id, th.BasicUser2.Id)
+ channel2M, _ := th.App.GetChannelMember(channel2.Id, th.BasicUser.Id)
+
+ assert.Equal(t, model.CHANNEL_NOTIFY_ALL, channel2M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP])
+
+ cmd := &MuteProvider{}
+
+ // First mute the channel
+ resp := cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ ChannelId: channel2.Id,
+ UserId: th.BasicUser.Id,
+ }, "")
+ assert.Equal(t, "api.command_mute.success_mute_direct_msg", resp.Text)
+ time.Sleep(time.Millisecond)
+ channel2M, _ = th.App.GetChannelMember(channel2.Id, th.BasicUser.Id)
+ assert.Equal(t, model.CHANNEL_NOTIFY_MENTION, channel2M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP])
+
+ // Now unmute the channel
+ resp = cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ ChannelId: channel2.Id,
+ UserId: th.BasicUser.Id,
+ }, "")
+
+ assert.Equal(t, "api.command_mute.success_unmute_direct_msg", resp.Text)
+ time.Sleep(time.Millisecond)
+ channel2M, _ = th.App.GetChannelMember(channel2.Id, th.BasicUser.Id)
+ assert.Equal(t, model.CHANNEL_NOTIFY_ALL, channel2M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP])
+}
diff --git a/app/config.go b/app/config.go
index 761fe3ec9..75d38e24a 100644
--- a/app/config.go
+++ b/app/config.go
@@ -30,6 +30,13 @@ func (a *App) Config() *model.Config {
return &model.Config{}
}
+func (a *App) EnvironmentConfig() map[string]interface{} {
+ if a.envConfig != nil {
+ return a.envConfig
+ }
+ return map[string]interface{}{}
+}
+
func (a *App) UpdateConfig(f func(*model.Config)) {
old := a.Config()
updated := old.Clone()
@@ -46,7 +53,7 @@ func (a *App) PersistConfig() {
func (a *App) LoadConfig(configFile string) *model.AppError {
old := a.Config()
- cfg, configPath, err := utils.LoadConfig(configFile)
+ cfg, configPath, envConfig, err := utils.LoadConfig(configFile)
if err != nil {
return err
}
@@ -57,6 +64,7 @@ func (a *App) LoadConfig(configFile string) *model.AppError {
l4g.Info("Using config file at %s", configPath)
a.config.Store(cfg)
+ a.envConfig = envConfig
a.siteURL = strings.TrimRight(*cfg.ServiceSettings.SiteURL, "/")
diff --git a/app/notification.go b/app/notification.go
index 5b7ed5ad2..2d56d294a 100644
--- a/app/notification.go
+++ b/app/notification.go
@@ -66,13 +66,25 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
}
}
- if _, ok := profileMap[otherUserId]; ok {
+ otherUser, ok := profileMap[otherUserId]
+ if ok {
mentionedUserIds[otherUserId] = true
}
if post.Props["from_webhook"] == "true" {
mentionedUserIds[post.UserId] = true
}
+
+ if post.Type != model.POST_AUTO_RESPONDER {
+ a.Go(func() {
+ rootId := post.Id
+ if post.RootId != "" && post.RootId != post.Id {
+ rootId = post.RootId
+ }
+ a.SendAutoResponse(channel, otherUser, rootId)
+ })
+ }
+
} else {
keywords := a.GetMentionKeywordsInChannel(profileMap, post.Type != model.POST_HEADER_CHANGE && post.Type != model.POST_PURPOSE_CHANGE)
@@ -842,15 +854,15 @@ func GetExplicitMentions(message string, keywords map[string][]string) *Explicit
checkForMention := func(word string) bool {
isMention := false
- if word == "@here" {
+ if strings.ToLower(word) == "@here" {
ret.HereMentioned = true
}
- if word == "@channel" {
+ if strings.ToLower(word) == "@channel" {
ret.ChannelMentioned = true
}
- if word == "@all" {
+ if strings.ToLower(word) == "@all" {
ret.AllMentioned = true
}
@@ -1021,8 +1033,8 @@ func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps m
}
func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *model.Status, channelId string) bool {
- // If User status is DND return false right away
- if status.Status == model.STATUS_DND {
+ // If User status is DND or OOO return false right away
+ if status.Status == model.STATUS_DND || status.Status == model.STATUS_OUT_OF_OFFICE {
return false
}
diff --git a/app/notification_test.go b/app/notification_test.go
index 1abdd3d61..8df19e2bf 100644
--- a/app/notification_test.go
+++ b/app/notification_test.go
@@ -186,6 +186,17 @@ func TestGetExplicitMentions(t *testing.T) {
ChannelMentioned: true,
},
},
+ "CapitalizedChannel": {
+ Message: "this is an message for @cHaNNeL",
+ Keywords: map[string][]string{"@channel": {id1, id2}},
+ Expected: &ExplicitMentions{
+ MentionedUserIds: map[string]bool{
+ id1: true,
+ id2: true,
+ },
+ ChannelMentioned: true,
+ },
+ },
"All": {
Message: "this is an message for @all",
Keywords: map[string][]string{"@all": {id1, id2}},
@@ -197,6 +208,17 @@ func TestGetExplicitMentions(t *testing.T) {
AllMentioned: true,
},
},
+ "CapitalizedAll": {
+ Message: "this is an message for @ALL",
+ Keywords: map[string][]string{"@all": {id1, id2}},
+ Expected: &ExplicitMentions{
+ MentionedUserIds: map[string]bool{
+ id1: true,
+ id2: true,
+ },
+ AllMentioned: true,
+ },
+ },
"UserWithPeriod": {
Message: "user.period doesn't complicate things at all by including periods in their username",
Keywords: map[string][]string{"user.period": {id1}, "user": {id2}},
@@ -439,6 +461,8 @@ func TestGetExplicitMentionsAtHere(t *testing.T) {
"?@here?": true,
"`@here`": false, // This case shouldn't mention since it's a code block
"~@here~": true,
+ "@HERE": true,
+ "@hERe": true,
}
for message, shouldMention := range cases {
diff --git a/app/plugin_api.go b/app/plugin_api.go
index 21b828368..b09a0c419 100644
--- a/app/plugin_api.go
+++ b/app/plugin_api.go
@@ -124,10 +124,35 @@ func (api *PluginAPI) UpdateChannel(channel *model.Channel) (*model.Channel, *mo
return api.app.UpdateChannel(channel)
}
+func (api *PluginAPI) AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) {
+ // For now, don't allow overriding these via the plugin API.
+ userRequestorId := ""
+ postRootId := ""
+
+ channel, err := api.GetChannel(channelId)
+ if err != nil {
+ return nil, err
+ }
+
+ return api.app.AddChannelMember(userId, channel, userRequestorId, postRootId)
+}
+
func (api *PluginAPI) GetChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) {
return api.app.GetChannelMember(channelId, userId)
}
+func (api *PluginAPI) UpdateChannelMemberRoles(channelId, userId, newRoles string) (*model.ChannelMember, *model.AppError) {
+ return api.app.UpdateChannelMemberRoles(channelId, userId, newRoles)
+}
+
+func (api *PluginAPI) UpdateChannelMemberNotifications(channelId, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) {
+ return api.app.UpdateChannelMemberNotifyProps(notifications, channelId, userId)
+}
+
+func (api *PluginAPI) DeleteChannelMember(channelId, userId string) *model.AppError {
+ return api.app.LeaveChannel(channelId, userId)
+}
+
func (api *PluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
return api.app.CreatePostMissingChannel(post, true)
}
diff --git a/app/status.go b/app/status.go
index c8bff0d1a..64d5379ef 100644
--- a/app/status.go
+++ b/app/status.go
@@ -303,6 +303,32 @@ func (a *App) SaveAndBroadcastStatus(status *model.Status) *model.AppError {
return nil
}
+func (a *App) SetStatusOutOfOffice(userId string) {
+ if !*a.Config().ServiceSettings.EnableUserStatuses {
+ return
+ }
+
+ status, err := a.GetStatus(userId)
+
+ if err != nil {
+ status = &model.Status{UserId: userId, Status: model.STATUS_OUT_OF_OFFICE, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
+ }
+
+ status.Status = model.STATUS_OUT_OF_OFFICE
+ status.Manual = true
+
+ a.AddStatusCache(status)
+
+ if result := <-a.Srv.Store.Status().SaveOrUpdate(status); result.Err != nil {
+ l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
+ }
+
+ event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil)
+ event.Add("status", model.STATUS_OUT_OF_OFFICE)
+ event.Add("user_id", status.UserId)
+ a.Publish(event)
+}
+
func GetStatusFromCache(userId string) *model.Status {
if result, ok := statusCache.Get(userId); ok {
status := result.(*model.Status)
diff --git a/cmd/commands/channel.go b/cmd/commands/channel.go
index 597a22450..d7b48f04c 100644
--- a/cmd/commands/channel.go
+++ b/cmd/commands/channel.go
@@ -31,7 +31,7 @@ var RemoveChannelUsersCmd = &cobra.Command{
Use: "remove [channel] [users]",
Short: "Remove users from channel",
Long: "Remove some users from channel",
- Example: " channel remove mychannel user@example.com username",
+ Example: " channel remove myteam:mychannel user@example.com username",
RunE: removeChannelUsersCmdF,
}
@@ -39,7 +39,7 @@ var AddChannelUsersCmd = &cobra.Command{
Use: "add [channel] [users]",
Short: "Add users to channel",
Long: "Add some users to channel",
- Example: " channel add mychannel user@example.com username",
+ Example: " channel add myteam:mychannel user@example.com username",
RunE: addChannelUsersCmdF,
}
diff --git a/cmd/commands/config_flag_test.go b/cmd/commands/config_flag_test.go
index 7ea0d5153..f31c989d8 100644
--- a/cmd/commands/config_flag_test.go
+++ b/cmd/commands/config_flag_test.go
@@ -22,7 +22,7 @@ func TestConfigFlag(t *testing.T) {
defer os.RemoveAll(dir)
utils.TranslationsPreInit()
- config, _, err := utils.LoadConfig("config.json")
+ config, _, _, err := utils.LoadConfig("config.json")
require.Nil(t, err)
configPath := filepath.Join(dir, "foo.json")
require.NoError(t, ioutil.WriteFile(configPath, []byte(config.ToJson()), 0600))
diff --git a/cmd/commands/message_export_test.go b/cmd/commands/message_export_test.go
index 5170b77af..bd0e049d6 100644
--- a/cmd/commands/message_export_test.go
+++ b/cmd/commands/message_export_test.go
@@ -57,7 +57,7 @@ func writeTempConfig(t *testing.T, isMessageExportEnabled bool) string {
require.NoError(t, err)
utils.TranslationsPreInit()
- config, _, appErr := utils.LoadConfig("config.json")
+ config, _, _, appErr := utils.LoadConfig("config.json")
require.Nil(t, appErr)
config.MessageExportSettings.EnableExport = model.NewBool(isMessageExportEnabled)
configPath := filepath.Join(dir, "foo.json")
diff --git a/cmd/commands/team.go b/cmd/commands/team.go
index 9c07b7456..05de714d6 100644
--- a/cmd/commands/team.go
+++ b/cmd/commands/team.go
@@ -52,6 +52,14 @@ Permanently deletes a team along with all related information including posts fr
RunE: deleteTeamsCmdF,
}
+var ListTeamsCmd = &cobra.Command{
+ Use: "list",
+ Short: "List all teams.",
+ Long: `List all teams on the server.`,
+ Example: " team list",
+ RunE: listTeamsCmdF,
+}
+
func init() {
TeamCreateCmd.Flags().String("name", "", "Team Name")
TeamCreateCmd.Flags().String("display_name", "", "Team Display Name")
@@ -65,6 +73,7 @@ func init() {
RemoveUsersCmd,
AddUsersCmd,
DeleteTeamsCmd,
+ ListTeamsCmd,
)
cmd.RootCmd.AddCommand(TeamCmd)
}
@@ -216,3 +225,21 @@ func deleteTeamsCmdF(command *cobra.Command, args []string) error {
func deleteTeam(a *app.App, team *model.Team) *model.AppError {
return a.PermanentDeleteTeam(team)
}
+
+func listTeamsCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
+ if err != nil {
+ return err
+ }
+
+ teams, err2 := a.GetAllTeams()
+ if err2 != nil {
+ return err2
+ }
+
+ for _, team := range teams {
+ cmd.CommandPrettyPrintln(team.Name)
+ }
+
+ return nil
+}
diff --git a/cmd/commands/team_test.go b/cmd/commands/team_test.go
index 1a91df4bc..ac006dbe1 100644
--- a/cmd/commands/team_test.go
+++ b/cmd/commands/team_test.go
@@ -4,6 +4,7 @@
package commands
import (
+ "strings"
"testing"
"github.com/mattermost/mattermost-server/api"
@@ -78,3 +79,20 @@ func TestLeaveTeam(t *testing.T) {
}
}
}
+
+func TestListTeams(t *testing.T) {
+ th := api.Setup().InitBasic()
+ defer th.TearDown()
+
+ id := model.NewId()
+ name := "name" + id
+ displayName := "Name " + id
+
+ cmd.CheckCommand(t, "team", "create", "--name", name, "--display_name", displayName)
+
+ output := cmd.CheckCommand(t, "team", "list", th.BasicTeam.Name, th.BasicUser.Email)
+
+ if !strings.Contains(string(output), name) {
+ t.Fatal("should have the created team")
+ }
+}
diff --git a/config/default.json b/config/default.json
index 56783753b..584380453 100644
--- a/config/default.json
+++ b/config/default.json
@@ -89,6 +89,7 @@
"MaxNotificationsPerChannel": 1000,
"EnableConfirmNotificationsToChannel": true,
"TeammateNameDisplay": "username",
+ "ExperimentalEnableAutomaticReplies": false,
"ExperimentalTownSquareIsReadOnly": false,
"ExperimentalPrimaryTeam": ""
},
diff --git a/glide.lock b/glide.lock
index 4915b477b..9fc53b688 100644
--- a/glide.lock
+++ b/glide.lock
@@ -208,7 +208,7 @@ imports:
- name: github.com/spf13/cast
version: 8965335b8c7107321228e3e3702cab9832751bac
- name: github.com/spf13/cobra
- version: be77323fc05148ef091e83b3866c0d47c8e74a8b
+ version: 4f5003aa93559718c866d86fbc795439079484f5
- name: github.com/spf13/jwalterweatherman
version: 7c0cea34c8ece3fbeb2b27ab9b59511d360fb394
- name: github.com/spf13/pflag
diff --git a/glide.yaml b/glide.yaml
index eb198b17e..d166f8573 100644
--- a/glide.yaml
+++ b/glide.yaml
@@ -50,6 +50,8 @@ import:
version: 2.1.1
- package: github.com/spf13/cobra
- package: github.com/spf13/viper
+ vcs: git
+ repo: https://github.com/hmhealey/viper.git
- package: github.com/stretchr/testify
version: v1.2.1
subpackages:
diff --git a/i18n/en.json b/i18n/en.json
index bf93e5b1c..02ac46e70 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -907,6 +907,14 @@
"translation": "Could not find the channel {{.Channel}}. Please use the [channel handle](https://about.mattermost.com/default-channel-handle-documentation) to identify channels."
},
{
+ "id": "api.command_mute.no_channel.error",
+ "translation": "Could not find the specified channel. Please use the [channel handle](https://about.mattermost.com/default-channel-handle-documentation) to identify channels."
+ },
+ {
+ "id": "api.command_mute.not_member.error",
+ "translation": "Could not mute channel {{.Channel}} as you are not a member."
+ },
+ {
"id": "api.command_mute.hint",
"translation": "~[channel]"
},
@@ -3087,6 +3095,10 @@
"translation": "Could not parse multipart form"
},
{
+ "id": "api.user.get_profile_image.not_found.app_error",
+ "translation": "Unable to get profile image, user not found."
+ },
+ {
"id": "api.user.upload_profile_user.storage.app_error",
"translation": "Unable to upload file. Image storage is not configured."
},
@@ -7328,13 +7340,17 @@
},
{
"id": "utils.mail.send_mail.from_address.app_error",
- "translation": "Notification From Address setting is missing or invalid."
+ "translation": "Error setting \"From Address\""
},
{
"id": "utils.mail.send_mail.msg.app_error",
"translation": "Failed to write email message"
},
{
+ "id": "app.admin.test_email.failure",
+ "translation": "Connection unsuccessful: {{.Error}}"
+ },
+ {
"id": "utils.mail.send_mail.msg_data.app_error",
"translation": "Failed to add email message data"
},
@@ -7344,7 +7360,7 @@
},
{
"id": "utils.mail.send_mail.to_address.app_error",
- "translation": "Notification To Address setting is missing or invalid."
+ "translation": "Error setting \"To Address\""
},
{
"id": "utils.mail.test.configured.error",
diff --git a/model/client4.go b/model/client4.go
index 3346cc6eb..82d380440 100644
--- a/model/client4.go
+++ b/model/client4.go
@@ -2167,6 +2167,18 @@ func (c *Client4) GetOldClientConfig(etag string) (map[string]string, *Response)
}
}
+// GetEnvironmentConfig will retrieve a map mirroring the server configuration where fields
+// are set to true if the corresponding config setting is set through an environment variable.
+// Settings that haven't been set through environment variables will be missing from the map.
+func (c *Client4) GetEnvironmentConfig() (map[string]interface{}, *Response) {
+ if r, err := c.DoApiGet(c.GetConfigRoute()+"/environment", ""); err != nil {
+ return nil, BuildErrorResponse(r, err)
+ } else {
+ defer closeBody(r)
+ return StringInterfaceFromJson(r.Body), BuildResponse(r)
+ }
+}
+
// GetOldClientLicense will retrieve the parts of the server license needed by the
// client, formatted in the old format.
func (c *Client4) GetOldClientLicense(etag string) (map[string]string, *Response) {
diff --git a/model/config.go b/model/config.go
index 6c6cf90e9..93533b8aa 100644
--- a/model/config.go
+++ b/model/config.go
@@ -991,6 +991,7 @@ type TeamSettings struct {
MaxNotificationsPerChannel *int64
EnableConfirmNotificationsToChannel *bool
TeammateNameDisplay *string
+ ExperimentalEnableAutomaticReplies *bool
ExperimentalTownSquareIsReadOnly *bool
ExperimentalPrimaryTeam *string
}
@@ -1085,6 +1086,10 @@ func (s *TeamSettings) SetDefaults() {
s.EnableConfirmNotificationsToChannel = NewBool(true)
}
+ if s.ExperimentalEnableAutomaticReplies == nil {
+ s.ExperimentalEnableAutomaticReplies = NewBool(false)
+ }
+
if s.ExperimentalTownSquareIsReadOnly == nil {
s.ExperimentalTownSquareIsReadOnly = NewBool(false)
}
@@ -1710,8 +1715,8 @@ func (s *MessageExportSettings) SetDefaults() {
if s.GlobalRelaySettings == nil {
s.GlobalRelaySettings = &GlobalRelayMessageExportSettings{}
- s.GlobalRelaySettings.SetDefaults()
}
+ s.GlobalRelaySettings.SetDefaults()
}
type DisplaySettings struct {
diff --git a/model/config_test.go b/model/config_test.go
index 1f917af27..b7533145b 100644
--- a/model/config_test.go
+++ b/model/config_test.go
@@ -4,11 +4,57 @@
package model
import (
+ "fmt"
+ "reflect"
"testing"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
+func TestConfigDefaults(t *testing.T) {
+ t.Parallel()
+
+ t.Run("somewhere nil when uninitialized", func(t *testing.T) {
+ c := Config{}
+ require.False(t, checkNowhereNil(t, "config", c))
+ })
+
+ t.Run("nowhere nil when initialized", func(t *testing.T) {
+ c := Config{}
+ c.SetDefaults()
+ require.True(t, checkNowhereNil(t, "config", c))
+ })
+
+ t.Run("nowhere nil when partially initialized", func(t *testing.T) {
+ var recursivelyUninitialize func(*Config, string, reflect.Value)
+ recursivelyUninitialize = func(config *Config, name string, v reflect.Value) {
+ if v.Type().Kind() == reflect.Ptr {
+ // Set every pointer we find in the tree to nil
+ v.Set(reflect.Zero(v.Type()))
+ require.True(t, v.IsNil())
+
+ // SetDefaults on the root config should make it non-nil, otherwise
+ // it means that SetDefaults isn't being called recursively in
+ // all cases.
+ config.SetDefaults()
+ if assert.False(t, v.IsNil(), "%s should be non-nil after SetDefaults()", name) {
+ recursivelyUninitialize(config, fmt.Sprintf("(*%s)", name), v.Elem())
+ }
+
+ } else if v.Type().Kind() == reflect.Struct {
+ for i := 0; i < v.NumField(); i++ {
+ recursivelyUninitialize(config, fmt.Sprintf("%s.%s", name, v.Type().Field(i).Name), v.Field(i))
+ }
+ }
+ }
+
+ c := Config{}
+ c.SetDefaults()
+ recursivelyUninitialize(&c, "config", reflect.ValueOf(&c).Elem())
+ })
+}
+
func TestConfigDefaultFileSettingsDirectory(t *testing.T) {
c1 := Config{}
c1.SetDefaults()
diff --git a/model/post.go b/model/post.go
index 09303c0cd..e74496979 100644
--- a/model/post.go
+++ b/model/post.go
@@ -25,6 +25,7 @@ const (
POST_LEAVE_CHANNEL = "system_leave_channel"
POST_JOIN_TEAM = "system_join_team"
POST_LEAVE_TEAM = "system_leave_team"
+ POST_AUTO_RESPONDER = "system_auto_responder"
POST_ADD_REMOVE = "system_add_remove" // Deprecated, use POST_ADD_TO_CHANNEL or POST_REMOVE_FROM_CHANNEL instead
POST_ADD_TO_CHANNEL = "system_add_to_channel"
POST_REMOVE_FROM_CHANNEL = "system_remove_from_channel"
@@ -194,6 +195,7 @@ func (o *Post) IsValid(maxPostSize int) *AppError {
case
POST_DEFAULT,
POST_JOIN_LEAVE,
+ POST_AUTO_RESPONDER,
POST_ADD_REMOVE,
POST_JOIN_CHANNEL,
POST_LEAVE_CHANNEL,
diff --git a/model/status.go b/model/status.go
index cd9e32ed3..cf5899446 100644
--- a/model/status.go
+++ b/model/status.go
@@ -9,6 +9,7 @@ import (
)
const (
+ STATUS_OUT_OF_OFFICE = "ooo"
STATUS_OFFLINE = "offline"
STATUS_AWAY = "away"
STATUS_DND = "dnd"
diff --git a/model/utils.go b/model/utils.go
index 72369852b..2d61b49f6 100644
--- a/model/utils.go
+++ b/model/utils.go
@@ -15,9 +15,11 @@ import (
"net/http"
"net/mail"
"net/url"
+ "reflect"
"regexp"
"strconv"
"strings"
+ "testing"
"time"
"unicode"
@@ -469,3 +471,60 @@ func IsValidId(value string) bool {
return true
}
+
+// checkNowhereNil checks that the given interface value is not nil, and if a struct, that all of
+// its public fields are also nowhere nil
+func checkNowhereNil(t *testing.T, name string, value interface{}) bool {
+ if value == nil {
+ return false
+ }
+
+ v := reflect.ValueOf(value)
+ switch v.Type().Kind() {
+ case reflect.Ptr:
+ if v.IsNil() {
+ t.Logf("%s was nil", name)
+ return false
+ }
+
+ return checkNowhereNil(t, fmt.Sprintf("(*%s)", name), v.Elem().Interface())
+
+ case reflect.Map:
+ if v.IsNil() {
+ t.Logf("%s was nil", name)
+ return false
+ }
+
+ // Don't check map values
+ return true
+
+ case reflect.Struct:
+ nowhereNil := true
+ for i := 0; i < v.NumField(); i++ {
+ f := v.Field(i)
+ // Ignore unexported fields
+ if v.Type().Field(i).PkgPath != "" {
+ continue
+ }
+
+ nowhereNil = nowhereNil && checkNowhereNil(t, fmt.Sprintf("%s.%s", name, v.Type().Field(i).Name), f.Interface())
+ }
+
+ return nowhereNil
+
+ case reflect.Array:
+ fallthrough
+ case reflect.Chan:
+ fallthrough
+ case reflect.Func:
+ fallthrough
+ case reflect.Interface:
+ fallthrough
+ case reflect.UnsafePointer:
+ t.Logf("unhandled field %s, type: %s", name, v.Type().Kind())
+ return false
+
+ default:
+ return true
+ }
+}
diff --git a/model/utils_test.go b/model/utils_test.go
index 411d7bf50..92354c0a1 100644
--- a/model/utils_test.go
+++ b/model/utils_test.go
@@ -7,6 +7,8 @@ import (
"net/http"
"strings"
"testing"
+
+ "github.com/stretchr/testify/require"
)
func TestNewId(t *testing.T) {
@@ -367,3 +369,184 @@ func TestIsValidId(t *testing.T) {
}
}
}
+
+func TestNowhereNil(t *testing.T) {
+ t.Parallel()
+
+ var nilStringPtr *string
+ var nonNilStringPtr *string = new(string)
+ var nilSlice []string
+ var nilStruct *struct{}
+ var nilMap map[bool]bool
+
+ var nowhereNilStruct = struct {
+ X *string
+ Y *string
+ }{
+ nonNilStringPtr,
+ nonNilStringPtr,
+ }
+ var somewhereNilStruct = struct {
+ X *string
+ Y *string
+ }{
+ nonNilStringPtr,
+ nilStringPtr,
+ }
+
+ var privateSomewhereNilStruct = struct {
+ X *string
+ y *string
+ }{
+ nonNilStringPtr,
+ nilStringPtr,
+ }
+
+ testCases := []struct {
+ Description string
+ Value interface{}
+ Expected bool
+ }{
+ {
+ "nil",
+ nil,
+ false,
+ },
+ {
+ "empty string",
+ "",
+ true,
+ },
+ {
+ "non-empty string",
+ "not empty!",
+ true,
+ },
+ {
+ "nil string pointer",
+ nilStringPtr,
+ false,
+ },
+ {
+ "non-nil string pointer",
+ nonNilStringPtr,
+ true,
+ },
+ {
+ "0",
+ 0,
+ true,
+ },
+ {
+ "1",
+ 1,
+ true,
+ },
+ {
+ "0 (int64)",
+ int64(0),
+ true,
+ },
+ {
+ "1 (int64)",
+ int64(1),
+ true,
+ },
+ {
+ "true",
+ true,
+ true,
+ },
+ {
+ "false",
+ false,
+ true,
+ },
+ {
+ "nil slice",
+ nilSlice,
+ // A nil slice is observably the same as an empty slice, so allow it.
+ true,
+ },
+ {
+ "empty slice",
+ []string{},
+ true,
+ },
+ {
+ "slice containing nils",
+ []*string{nil, nil},
+ true,
+ },
+ {
+ "nil map",
+ nilMap,
+ false,
+ },
+ {
+ "non-nil map",
+ make(map[bool]bool),
+ true,
+ },
+ {
+ "non-nil map containing nil",
+ map[bool]*string{true: nilStringPtr, false: nonNilStringPtr},
+ // Map values are not checked
+ true,
+ },
+ {
+ "nil struct",
+ nilStruct,
+ false,
+ },
+ {
+ "empty struct",
+ struct{}{},
+ true,
+ },
+ {
+ "struct containing no nil",
+ nowhereNilStruct,
+ true,
+ },
+ {
+ "struct containing nil",
+ somewhereNilStruct,
+ false,
+ },
+ {
+ "struct pointer containing no nil",
+ &nowhereNilStruct,
+ true,
+ },
+ {
+ "struct pointer containing nil",
+ &somewhereNilStruct,
+ false,
+ },
+ {
+ "struct containing private nil",
+ privateSomewhereNilStruct,
+ true,
+ },
+ {
+ "struct pointer containing private nil",
+ &privateSomewhereNilStruct,
+ true,
+ },
+ }
+
+ for _, testCase := range testCases {
+ testCase := testCase
+ t.Run(testCase.Description, func(t *testing.T) {
+ defer func() {
+ if r := recover(); r != nil {
+ t.Errorf("panic: %v", r)
+ }
+ }()
+
+ t.Parallel()
+ require.Equal(t, testCase.Expected, checkNowhereNil(t, "value", testCase.Value))
+ })
+ }
+}
diff --git a/model/websocket_client.go b/model/websocket_client.go
index cdec75aba..4ff4f617b 100644
--- a/model/websocket_client.go
+++ b/model/websocket_client.go
@@ -29,7 +29,13 @@ type WebSocketClient struct {
// NewWebSocketClient constructs a new WebSocket client with convenience
// methods for talking to the server.
func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) {
- conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX_V3+"/users/websocket", nil)
+ return NewWebSocketClientWithDialer(websocket.DefaultDialer, url, authToken)
+}
+
+// NewWebSocketClientWithDialer constructs a new WebSocket client with convenience
+// methods for talking to the server using a custom dialer.
+func NewWebSocketClientWithDialer(dialer *websocket.Dialer, url, authToken string) (*WebSocketClient, *AppError) {
+ conn, _, err := dialer.Dial(url+API_URL_SUFFIX_V3+"/users/websocket", nil)
if err != nil {
return nil, NewAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError)
}
@@ -54,7 +60,13 @@ func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) {
// NewWebSocketClient4 constructs a new WebSocket client with convenience
// methods for talking to the server. Uses the v4 endpoint.
func NewWebSocketClient4(url, authToken string) (*WebSocketClient, *AppError) {
- conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX+"/websocket", nil)
+ return NewWebSocketClient4WithDialer(websocket.DefaultDialer, url, authToken)
+}
+
+// NewWebSocketClient4WithDialer constructs a new WebSocket client with convenience
+// methods for talking to the server using a custom dialer. Uses the v4 endpoint.
+func NewWebSocketClient4WithDialer(dialer *websocket.Dialer, url, authToken string) (*WebSocketClient, *AppError) {
+ conn, _, err := dialer.Dial(url+API_URL_SUFFIX+"/websocket", nil)
if err != nil {
return nil, NewAppError("NewWebSocketClient4", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError)
}
@@ -77,8 +89,12 @@ func NewWebSocketClient4(url, authToken string) (*WebSocketClient, *AppError) {
}
func (wsc *WebSocketClient) Connect() *AppError {
+ return wsc.ConnectWithDialer(websocket.DefaultDialer)
+}
+
+func (wsc *WebSocketClient) ConnectWithDialer(dialer *websocket.Dialer) *AppError {
var err error
- wsc.Conn, _, err = websocket.DefaultDialer.Dial(wsc.ConnectUrl, nil)
+ wsc.Conn, _, err = dialer.Dial(wsc.ConnectUrl, nil)
if err != nil {
return NewAppError("Connect", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError)
}
diff --git a/plugin/api.go b/plugin/api.go
index 437188f6e..d62c2f069 100644
--- a/plugin/api.go
+++ b/plugin/api.go
@@ -77,9 +77,21 @@ type API interface {
// UpdateChannel updates a channel.
UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError)
+ // AddChannelMember creates a channel membership for a user.
+ AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError)
+
// GetChannelMember gets a channel membership for a user.
GetChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError)
+ // UpdateChannelMemberRoles updates a user's roles for a channel.
+ UpdateChannelMemberRoles(channelId, userId, newRoles string) (*model.ChannelMember, *model.AppError)
+
+ // UpdateChannelMemberNotifications updates a user's notification properties for a channel.
+ UpdateChannelMemberNotifications(channelId, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError)
+
+ // DeleteChannelMember deletes a channel membership for a user.
+ DeleteChannelMember(channelId, userId string) *model.AppError
+
// CreatePost creates a post.
CreatePost(post *model.Post) (*model.Post, *model.AppError)
diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go
index 75174a9a6..8f9f4a604 100644
--- a/plugin/plugintest/api.go
+++ b/plugin/plugintest/api.go
@@ -223,6 +223,16 @@ func (m *API) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppE
return channelOut, err
}
+func (m *API) AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) {
+ ret := m.Called(channelId, userId)
+ if f, ok := ret.Get(0).(func(_, _ string) (*model.ChannelMember, *model.AppError)); ok {
+ return f(channelId, userId)
+ }
+ member, _ := ret.Get(0).(*model.ChannelMember)
+ err, _ := ret.Get(1).(*model.AppError)
+ return member, err
+}
+
func (m *API) GetChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) {
ret := m.Called(channelId, userId)
if f, ok := ret.Get(0).(func(_, _ string) (*model.ChannelMember, *model.AppError)); ok {
@@ -233,6 +243,35 @@ func (m *API) GetChannelMember(channelId, userId string) (*model.ChannelMember,
return member, err
}
+func (m *API) UpdateChannelMemberRoles(channelId, userId, newRoles string) (*model.ChannelMember, *model.AppError) {
+ ret := m.Called(channelId, userId, newRoles)
+ if f, ok := ret.Get(0).(func(_, _, _ string) (*model.ChannelMember, *model.AppError)); ok {
+ return f(channelId, userId, newRoles)
+ }
+ member, _ := ret.Get(0).(*model.ChannelMember)
+ err, _ := ret.Get(1).(*model.AppError)
+ return member, err
+}
+
+func (m *API) UpdateChannelMemberNotifications(channelId, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) {
+ ret := m.Called(channelId, userId, notifications)
+ if f, ok := ret.Get(0).(func(_, _ string, _ map[string]string) (*model.ChannelMember, *model.AppError)); ok {
+ return f(channelId, userId, notifications)
+ }
+ member, _ := ret.Get(0).(*model.ChannelMember)
+ err, _ := ret.Get(1).(*model.AppError)
+ return member, err
+}
+
+func (m *API) DeleteChannelMember(channelId, userId string) *model.AppError {
+ ret := m.Called(channelId, userId)
+ if f, ok := ret.Get(0).(func(_, _ string) *model.AppError); ok {
+ return f(channelId, userId)
+ }
+ err, _ := ret.Get(0).(*model.AppError)
+ return err
+}
+
func (m *API) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
ret := m.Called(post)
if f, ok := ret.Get(0).(func(*model.Post) (*model.Post, *model.AppError)); ok {
diff --git a/plugin/rpcplugin/api.go b/plugin/rpcplugin/api.go
index d87f65b55..c81bbb7c5 100644
--- a/plugin/rpcplugin/api.go
+++ b/plugin/rpcplugin/api.go
@@ -163,11 +163,33 @@ type APIGetGroupChannelArgs struct {
UserIds []string
}
+type APIAddChannelMemberArgs struct {
+ ChannelId string
+ UserId string
+}
+
type APIGetChannelMemberArgs struct {
ChannelId string
UserId string
}
+type APIUpdateChannelMemberRolesArgs struct {
+ ChannelId string
+ UserId string
+ NewRoles string
+}
+
+type APIUpdateChannelMemberNotificationsArgs struct {
+ ChannelId string
+ UserId string
+ Notifications map[string]string
+}
+
+type APIDeleteChannelMemberArgs struct {
+ ChannelId string
+ UserId string
+}
+
type APIChannelReply struct {
Channel *model.Channel
Error *model.AppError
@@ -239,6 +261,15 @@ func (api *LocalAPI) UpdateChannel(args *model.Channel, reply *APIChannelReply)
return nil
}
+func (api *LocalAPI) AddChannelMember(args *APIAddChannelMemberArgs, reply *APIChannelMemberReply) error {
+ member, err := api.api.AddChannelMember(args.ChannelId, args.UserId)
+ *reply = APIChannelMemberReply{
+ ChannelMember: member,
+ Error: err,
+ }
+ return nil
+}
+
func (api *LocalAPI) GetChannelMember(args *APIGetChannelMemberArgs, reply *APIChannelMemberReply) error {
member, err := api.api.GetChannelMember(args.ChannelId, args.UserId)
*reply = APIChannelMemberReply{
@@ -248,6 +279,32 @@ func (api *LocalAPI) GetChannelMember(args *APIGetChannelMemberArgs, reply *APIC
return nil
}
+func (api *LocalAPI) UpdateChannelMemberRoles(args *APIUpdateChannelMemberRolesArgs, reply *APIChannelMemberReply) error {
+ member, err := api.api.UpdateChannelMemberRoles(args.ChannelId, args.UserId, args.NewRoles)
+ *reply = APIChannelMemberReply{
+ ChannelMember: member,
+ Error: err,
+ }
+ return nil
+}
+
+func (api *LocalAPI) UpdateChannelMemberNotifications(args *APIUpdateChannelMemberNotificationsArgs, reply *APIChannelMemberReply) error {
+ member, err := api.api.UpdateChannelMemberNotifications(args.ChannelId, args.UserId, args.Notifications)
+ *reply = APIChannelMemberReply{
+ ChannelMember: member,
+ Error: err,
+ }
+ return nil
+}
+
+func (api *LocalAPI) DeleteChannelMember(args *APIDeleteChannelMemberArgs, reply *APIErrorReply) error {
+ err := api.api.DeleteChannelMember(args.ChannelId, args.UserId)
+ *reply = APIErrorReply{
+ Error: err,
+ }
+ return nil
+}
+
type APIPostReply struct {
Post *model.Post
Error *model.AppError
@@ -520,6 +577,17 @@ func (api *RemoteAPI) UpdateChannel(channel *model.Channel) (*model.Channel, *mo
return reply.Channel, reply.Error
}
+func (api *RemoteAPI) AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) {
+ var reply APIChannelMemberReply
+ if err := api.client.Call("LocalAPI.AddChannelMember", &APIAddChannelMemberArgs{
+ ChannelId: channelId,
+ UserId: userId,
+ }, &reply); err != nil {
+ return nil, model.NewAppError("RemoteAPI.AddChannelMember", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
+ }
+ return reply.ChannelMember, reply.Error
+}
+
func (api *RemoteAPI) GetChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) {
var reply APIChannelMemberReply
if err := api.client.Call("LocalAPI.GetChannelMember", &APIGetChannelMemberArgs{
@@ -531,6 +599,41 @@ func (api *RemoteAPI) GetChannelMember(channelId, userId string) (*model.Channel
return reply.ChannelMember, reply.Error
}
+func (api *RemoteAPI) UpdateChannelMemberRoles(channelId, userId, newRoles string) (*model.ChannelMember, *model.AppError) {
+ var reply APIChannelMemberReply
+ if err := api.client.Call("LocalAPI.UpdateChannelMemberRoles", &APIUpdateChannelMemberRolesArgs{
+ ChannelId: channelId,
+ UserId: userId,
+ NewRoles: newRoles,
+ }, &reply); err != nil {
+ return nil, model.NewAppError("RemoteAPI.UpdateChannelMemberRoles", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
+ }
+ return reply.ChannelMember, reply.Error
+}
+
+func (api *RemoteAPI) UpdateChannelMemberNotifications(channelId, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) {
+ var reply APIChannelMemberReply
+ if err := api.client.Call("LocalAPI.UpdateChannelMemberNotifications", &APIUpdateChannelMemberNotificationsArgs{
+ ChannelId: channelId,
+ UserId: userId,
+ Notifications: notifications,
+ }, &reply); err != nil {
+ return nil, model.NewAppError("RemoteAPI.UpdateChannelMemberNotifications", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
+ }
+ return reply.ChannelMember, reply.Error
+}
+
+func (api *RemoteAPI) DeleteChannelMember(channelId, userId string) *model.AppError {
+ var reply APIErrorReply
+ if err := api.client.Call("LocalAPI.DeleteChannelMember", &APIDeleteChannelMemberArgs{
+ ChannelId: channelId,
+ UserId: userId,
+ }, &reply); err != nil {
+ return model.NewAppError("RemoteAPI.DeleteChannelMember", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
+ }
+ return reply.Error
+}
+
func (api *RemoteAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
var reply APIPostReply
if err := api.client.Call("LocalAPI.CreatePost", post, &reply); err != nil {
diff --git a/plugin/rpcplugin/api_test.go b/plugin/rpcplugin/api_test.go
index 7fe7a0ff9..d7b3733ea 100644
--- a/plugin/rpcplugin/api_test.go
+++ b/plugin/rpcplugin/api_test.go
@@ -128,11 +128,32 @@ func TestAPI(t *testing.T) {
assert.Equal(t, testChannel, channel)
assert.Nil(t, err)
+ api.On("AddChannelMember", testChannel.Id, "theuserid").Return(testChannelMember, nil).Once()
+ member, err := remote.AddChannelMember(testChannel.Id, "theuserid")
+ assert.Equal(t, testChannelMember, member)
+ assert.Nil(t, err)
+
api.On("GetChannelMember", "thechannelid", "theuserid").Return(testChannelMember, nil).Once()
- member, err := remote.GetChannelMember("thechannelid", "theuserid")
+ member, err = remote.GetChannelMember("thechannelid", "theuserid")
+ assert.Equal(t, testChannelMember, member)
+ assert.Nil(t, err)
+
+ api.On("UpdateChannelMemberRoles", testChannel.Id, "theuserid", model.CHANNEL_ADMIN_ROLE_ID).Return(testChannelMember, nil).Once()
+ member, err = remote.UpdateChannelMemberRoles(testChannel.Id, "theuserid", model.CHANNEL_ADMIN_ROLE_ID)
assert.Equal(t, testChannelMember, member)
assert.Nil(t, err)
+ notifications := map[string]string{}
+ notifications[model.MARK_UNREAD_NOTIFY_PROP] = model.CHANNEL_MARK_UNREAD_MENTION
+ api.On("UpdateChannelMemberNotifications", testChannel.Id, "theuserid", notifications).Return(testChannelMember, nil).Once()
+ member, err = remote.UpdateChannelMemberNotifications(testChannel.Id, "theuserid", notifications)
+ assert.Equal(t, testChannelMember, member)
+ assert.Nil(t, err)
+
+ api.On("DeleteChannelMember", "thechannelid", "theuserid").Return(nil).Once()
+ err = remote.DeleteChannelMember("thechannelid", "theuserid")
+ assert.Nil(t, err)
+
api.On("CreateUser", mock.AnythingOfType("*model.User")).Return(func(u *model.User) (*model.User, *model.AppError) {
u.Id = "theuserid"
return u, nil
diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go
index ae0ea7acd..882ee48ba 100644
--- a/store/sqlstore/upgrade.go
+++ b/store/sqlstore/upgrade.go
@@ -16,6 +16,7 @@ import (
)
const (
+ VERSION_4_10_0 = "4.10.0"
VERSION_4_9_0 = "4.9.0"
VERSION_4_8_1 = "4.8.1"
VERSION_4_8_0 = "4.8.0"
@@ -75,6 +76,7 @@ func UpgradeDatabase(sqlStore SqlStore) {
UpgradeDatabaseToVersion48(sqlStore)
UpgradeDatabaseToVersion481(sqlStore)
UpgradeDatabaseToVersion49(sqlStore)
+ UpgradeDatabaseToVersion410(sqlStore)
// If the SchemaVersion is empty this this is the first time it has ran
// so lets set it to the current version.
@@ -408,3 +410,11 @@ func UpgradeDatabaseToVersion49(sqlStore SqlStore) {
saveSchemaVersion(sqlStore, VERSION_4_9_0)
}
}
+
+func UpgradeDatabaseToVersion410(sqlStore SqlStore) {
+ // TODO: Uncomment following condition when version 4.10.0 is released
+ //if shouldPerformUpgrade(sqlStore, VERSION_4_9_0, VERSION_4_10_0) {
+
+ // saveSchemaVersion(sqlStore, VERSION_4_10_0)
+ //}
+}
diff --git a/utils/config.go b/utils/config.go
index 13295b362..7032dbad9 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -10,6 +10,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
+ "reflect"
"strconv"
"strings"
@@ -187,7 +188,7 @@ func NewConfigWatcher(cfgFileName string, f func()) (*ConfigWatcher, error) {
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
l4g.Info(fmt.Sprintf("Config file watcher detected a change reloading %v", cfgFileName))
- if _, configReadErr := ReadConfigFile(cfgFileName, true); configReadErr == nil {
+ if _, _, configReadErr := ReadConfigFile(cfgFileName, true); configReadErr == nil {
f()
} else {
l4g.Error(fmt.Sprintf("Failed to read while watching config file at %v with err=%v", cfgFileName, configReadErr.Error()))
@@ -211,18 +212,11 @@ func (w *ConfigWatcher) Close() {
}
// ReadConfig reads and parses the given configuration.
-func ReadConfig(r io.Reader, allowEnvironmentOverrides bool) (*model.Config, error) {
- v := viper.New()
+func ReadConfig(r io.Reader, allowEnvironmentOverrides bool) (*model.Config, map[string]interface{}, error) {
+ v := newViper(allowEnvironmentOverrides)
- if allowEnvironmentOverrides {
- v.SetEnvPrefix("mm")
- v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
- v.AutomaticEnv()
- }
-
- v.SetConfigType("json")
if err := v.ReadConfig(r); err != nil {
- return nil, err
+ return nil, nil, err
}
var config model.Config
@@ -233,14 +227,151 @@ func ReadConfig(r io.Reader, allowEnvironmentOverrides bool) (*model.Config, err
config.PluginSettings = model.PluginSettings{}
unmarshalErr = v.UnmarshalKey("pluginsettings", &config.PluginSettings)
}
- return &config, unmarshalErr
+
+ envConfig := v.EnvSettings()
+
+ var envErr error
+ if envConfig, envErr = fixEnvSettingsCase(envConfig); envErr != nil {
+ return nil, nil, envErr
+ }
+
+ return &config, envConfig, unmarshalErr
+}
+
+func newViper(allowEnvironmentOverrides bool) *viper.Viper {
+ v := viper.New()
+
+ v.SetConfigType("json")
+
+ if allowEnvironmentOverrides {
+ v.SetEnvPrefix("mm")
+ v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
+ v.AutomaticEnv()
+ }
+
+ // Set zeroed defaults for all the config settings so that Viper knows what environment variables
+ // it needs to be looking for. The correct defaults will later be applied using Config.SetDefaults.
+ defaults := flattenStructToMap(structToMap(reflect.TypeOf(model.Config{})))
+
+ for key, value := range defaults {
+ v.SetDefault(key, value)
+ }
+
+ return v
+}
+
+// Converts a struct type into a nested map with keys matching the struct's fields and values
+// matching the zeroed value of the corresponding field.
+func structToMap(t reflect.Type) (out map[string]interface{}) {
+ defer func() {
+ if r := recover(); r != nil {
+ l4g.Error("Panicked in structToMap. This should never happen. %v", r)
+ }
+ }()
+
+ if t.Kind() != reflect.Struct {
+ // Should never hit this, but this will prevent a panic if that does happen somehow
+ return nil
+ }
+
+ out = map[string]interface{}{}
+
+ for i := 0; i < t.NumField(); i++ {
+ field := t.Field(i)
+
+ var value interface{}
+
+ switch field.Type.Kind() {
+ case reflect.Struct:
+ value = structToMap(field.Type)
+ case reflect.Ptr:
+ value = nil
+ default:
+ value = reflect.Zero(field.Type).Interface()
+ }
+
+ out[field.Name] = value
+ }
+
+ return
+}
+
+// Flattens a nested map so that the result is a single map with keys corresponding to the
+// path through the original map. For example,
+// {
+// "a": {
+// "b": 1
+// },
+// "c": "sea"
+// }
+// would flatten to
+// {
+// "a.b": 1,
+// "c": "sea"
+// }
+func flattenStructToMap(in map[string]interface{}) map[string]interface{} {
+ out := make(map[string]interface{})
+
+ for key, value := range in {
+ if valueAsMap, ok := value.(map[string]interface{}); ok {
+ sub := flattenStructToMap(valueAsMap)
+
+ for subKey, subValue := range sub {
+ out[key+"."+subKey] = subValue
+ }
+ } else {
+ out[key] = value
+ }
+ }
+
+ return out
+}
+
+// Fixes the case of the environment variables sent back from Viper since Viper stores
+// everything as lower case.
+func fixEnvSettingsCase(in map[string]interface{}) (out map[string]interface{}, err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ l4g.Error("Panicked in fixEnvSettingsCase. This should never happen. %v", r)
+ out = in
+ }
+ }()
+
+ var fixCase func(map[string]interface{}, reflect.Type) map[string]interface{}
+ fixCase = func(in map[string]interface{}, t reflect.Type) map[string]interface{} {
+ if t.Kind() != reflect.Struct {
+ // Should never hit this, but this will prevent a panic if that does happen somehow
+ return nil
+ }
+
+ out := make(map[string]interface{}, len(in))
+
+ for i := 0; i < t.NumField(); i++ {
+ field := t.Field(i)
+
+ key := field.Name
+ if value, ok := in[strings.ToLower(key)]; ok {
+ if valueAsMap, ok := value.(map[string]interface{}); ok {
+ out[key] = fixCase(valueAsMap, field.Type)
+ } else {
+ out[key] = value
+ }
+ }
+ }
+
+ return out
+ }
+
+ out = fixCase(in, reflect.TypeOf(model.Config{}))
+
+ return
}
// ReadConfigFile reads and parses the configuration at the given file path.
-func ReadConfigFile(path string, allowEnvironmentOverrides bool) (*model.Config, error) {
+func ReadConfigFile(path string, allowEnvironmentOverrides bool) (*model.Config, map[string]interface{}, error) {
f, err := os.Open(path)
if err != nil {
- return nil, err
+ return nil, nil, err
}
defer f.Close()
return ReadConfig(f, allowEnvironmentOverrides)
@@ -275,22 +406,24 @@ func EnsureConfigFile(fileName string) (string, error) {
// LoadConfig will try to search around for the corresponding config file. It will search
// /tmp/fileName then attempt ./config/fileName, then ../config/fileName and last it will look at
// fileName.
-func LoadConfig(fileName string) (config *model.Config, configPath string, appErr *model.AppError) {
+func LoadConfig(fileName string) (*model.Config, string, map[string]interface{}, *model.AppError) {
+ var configPath string
+
if fileName != filepath.Base(fileName) {
configPath = fileName
} else {
if path, err := EnsureConfigFile(fileName); err != nil {
- appErr = model.NewAppError("LoadConfig", "utils.config.load_config.opening.panic", map[string]interface{}{"Filename": fileName, "Error": err.Error()}, "", 0)
- return
+ appErr := model.NewAppError("LoadConfig", "utils.config.load_config.opening.panic", map[string]interface{}{"Filename": fileName, "Error": err.Error()}, "", 0)
+ return nil, "", nil, appErr
} else {
configPath = path
}
}
- config, err := ReadConfigFile(configPath, true)
+ config, envConfig, err := ReadConfigFile(configPath, true)
if err != nil {
- appErr = model.NewAppError("LoadConfig", "utils.config.load_config.decoding.panic", map[string]interface{}{"Filename": fileName, "Error": err.Error()}, "", 0)
- return
+ appErr := model.NewAppError("LoadConfig", "utils.config.load_config.decoding.panic", map[string]interface{}{"Filename": fileName, "Error": err.Error()}, "", 0)
+ return nil, "", nil, appErr
}
needSave := len(config.SqlSettings.AtRestEncryptKey) == 0 || len(*config.FileSettings.PublicLinkSalt) == 0 ||
@@ -299,7 +432,7 @@ func LoadConfig(fileName string) (config *model.Config, configPath string, appEr
config.SetDefaults()
if err := config.IsValid(); err != nil {
- return nil, "", err
+ return nil, "", nil, err
}
if needSave {
@@ -321,7 +454,7 @@ func LoadConfig(fileName string) (config *model.Config, configPath string, appEr
}
}
- return config, configPath, nil
+ return config, configPath, envConfig, nil
}
func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.License) map[string]string {
@@ -382,6 +515,7 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L
props["EnableTutorial"] = strconv.FormatBool(*c.ServiceSettings.EnableTutorial)
props["ExperimentalEnableDefaultChannelLeaveJoinMessages"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages)
props["ExperimentalGroupUnreadChannels"] = *c.ServiceSettings.ExperimentalGroupUnreadChannels
+ props["ExperimentalEnableAutomaticReplies"] = strconv.FormatBool(*c.TeamSettings.ExperimentalEnableAutomaticReplies)
props["ExperimentalTimezone"] = strconv.FormatBool(*c.DisplaySettings.ExperimentalTimezone)
props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications)
diff --git a/utils/config_test.go b/utils/config_test.go
index 84e7291b0..fbac577ee 100644
--- a/utils/config_test.go
+++ b/utils/config_test.go
@@ -18,7 +18,7 @@ import (
func TestConfig(t *testing.T) {
TranslationsPreInit()
- cfg, _, err := LoadConfig("config.json")
+ cfg, _, _, err := LoadConfig("config.json")
require.Nil(t, err)
InitTranslations(cfg.LocalizationSettings)
}
@@ -50,53 +50,133 @@ func TestFindConfigFile(t *testing.T) {
}
func TestConfigFromEnviroVars(t *testing.T) {
- os.Setenv("MM_TEAMSETTINGS_SITENAME", "From Environment")
- os.Setenv("MM_TEAMSETTINGS_CUSTOMBRANDTEXT", "Custom Brand")
- os.Setenv("MM_SERVICESETTINGS_ENABLECOMMANDS", "false")
- os.Setenv("MM_SERVICESETTINGS_READTIMEOUT", "400")
-
TranslationsPreInit()
- cfg, cfgPath, err := LoadConfig("config.json")
- require.Nil(t, err)
- if cfg.TeamSettings.SiteName != "From Environment" {
- t.Fatal("Couldn't read config from environment var")
- }
+ config := `{
+ "ServiceSettings": {
+ "EnableCommands": true,
+ "ReadTimeout": 100
+ },
+ "TeamSettings": {
+ "SiteName": "Mattermost",
+ "CustomBrandText": ""
+ }
+ }`
- if *cfg.TeamSettings.CustomBrandText != "Custom Brand" {
- t.Fatal("Couldn't read config from environment var")
- }
+ t.Run("string settings", func(t *testing.T) {
+ os.Setenv("MM_TEAMSETTINGS_SITENAME", "From Environment")
+ os.Setenv("MM_TEAMSETTINGS_CUSTOMBRANDTEXT", "Custom Brand")
- if *cfg.ServiceSettings.EnableCommands {
- t.Fatal("Couldn't read config from environment var")
- }
+ cfg, envCfg, err := ReadConfig(strings.NewReader(config), true)
+ require.Nil(t, err)
- if *cfg.ServiceSettings.ReadTimeout != 400 {
- t.Fatal("Couldn't read config from environment var")
- }
+ if cfg.TeamSettings.SiteName != "From Environment" {
+ t.Fatal("Couldn't read config from environment var")
+ }
- os.Unsetenv("MM_TEAMSETTINGS_SITENAME")
- os.Unsetenv("MM_TEAMSETTINGS_CUSTOMBRANDTEXT")
- os.Unsetenv("MM_SERVICESETTINGS_ENABLECOMMANDS")
- os.Unsetenv("MM_SERVICESETTINGS_READTIMEOUT")
+ if *cfg.TeamSettings.CustomBrandText != "Custom Brand" {
+ t.Fatal("Couldn't read config from environment var")
+ }
- cfg.TeamSettings.SiteName = "Mattermost"
- *cfg.ServiceSettings.SiteURL = ""
- *cfg.ServiceSettings.EnableCommands = true
- *cfg.ServiceSettings.ReadTimeout = 300
- SaveConfig(cfgPath, cfg)
+ if teamSettings, ok := envCfg["TeamSettings"]; !ok {
+ t.Fatal("TeamSettings is missing from envConfig")
+ } else if teamSettingsAsMap, ok := teamSettings.(map[string]interface{}); !ok {
+ t.Fatal("TeamSettings is not a map in envConfig")
+ } else {
+ if siteNameInEnv, ok := teamSettingsAsMap["SiteName"].(bool); !ok || !siteNameInEnv {
+ t.Fatal("SiteName should be in envConfig")
+ }
- cfg, _, err = LoadConfig("config.json")
- require.Nil(t, err)
+ if customBrandTextInEnv, ok := teamSettingsAsMap["CustomBrandText"].(bool); !ok || !customBrandTextInEnv {
+ t.Fatal("SiteName should be in envConfig")
+ }
+ }
- if cfg.TeamSettings.SiteName != "Mattermost" {
- t.Fatal("should have been reset")
- }
+ os.Unsetenv("MM_TEAMSETTINGS_SITENAME")
+ os.Unsetenv("MM_TEAMSETTINGS_CUSTOMBRANDTEXT")
+
+ cfg, envCfg, err = ReadConfig(strings.NewReader(config), true)
+ require.Nil(t, err)
+
+ if cfg.TeamSettings.SiteName != "Mattermost" {
+ t.Fatal("should have been reset")
+ }
+
+ if _, ok := envCfg["TeamSettings"]; ok {
+ t.Fatal("TeamSettings should be missing from envConfig")
+ }
+ })
+
+ t.Run("boolean setting", func(t *testing.T) {
+ os.Setenv("MM_SERVICESETTINGS_ENABLECOMMANDS", "false")
+ defer os.Unsetenv("MM_SERVICESETTINGS_ENABLECOMMANDS")
+
+ cfg, envCfg, err := ReadConfig(strings.NewReader(config), true)
+ require.Nil(t, err)
+
+ if *cfg.ServiceSettings.EnableCommands {
+ t.Fatal("Couldn't read config from environment var")
+ }
+
+ if serviceSettings, ok := envCfg["ServiceSettings"]; !ok {
+ t.Fatal("ServiceSettings is missing from envConfig")
+ } else if serviceSettingsAsMap, ok := serviceSettings.(map[string]interface{}); !ok {
+ t.Fatal("ServiceSettings is not a map in envConfig")
+ } else {
+ if enableCommandsInEnv, ok := serviceSettingsAsMap["EnableCommands"].(bool); !ok || !enableCommandsInEnv {
+ t.Fatal("EnableCommands should be in envConfig")
+ }
+ }
+ })
+
+ t.Run("integer setting", func(t *testing.T) {
+ os.Setenv("MM_SERVICESETTINGS_READTIMEOUT", "400")
+ defer os.Unsetenv("MM_SERVICESETTINGS_READTIMEOUT")
+
+ cfg, envCfg, err := ReadConfig(strings.NewReader(config), true)
+ require.Nil(t, err)
+
+ if *cfg.ServiceSettings.ReadTimeout != 400 {
+ t.Fatal("Couldn't read config from environment var")
+ }
+
+ if serviceSettings, ok := envCfg["ServiceSettings"]; !ok {
+ t.Fatal("ServiceSettings is missing from envConfig")
+ } else if serviceSettingsAsMap, ok := serviceSettings.(map[string]interface{}); !ok {
+ t.Fatal("ServiceSettings is not a map in envConfig")
+ } else {
+ if readTimeoutInEnv, ok := serviceSettingsAsMap["ReadTimeout"].(bool); !ok || !readTimeoutInEnv {
+ t.Fatal("ReadTimeout should be in envConfig")
+ }
+ }
+ })
+
+ t.Run("setting missing from config.json", func(t *testing.T) {
+ os.Setenv("MM_SERVICESETTINGS_SITEURL", "https://example.com")
+ defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
+
+ cfg, envCfg, err := ReadConfig(strings.NewReader(config), true)
+ require.Nil(t, err)
+
+ if *cfg.ServiceSettings.SiteURL != "https://example.com" {
+ t.Fatal("Couldn't read config from environment var")
+ }
+
+ if serviceSettings, ok := envCfg["ServiceSettings"]; !ok {
+ t.Fatal("ServiceSettings is missing from envConfig")
+ } else if serviceSettingsAsMap, ok := serviceSettings.(map[string]interface{}); !ok {
+ t.Fatal("ServiceSettings is not a map in envConfig")
+ } else {
+ if siteURLInEnv, ok := serviceSettingsAsMap["SiteURL"].(bool); !ok || !siteURLInEnv {
+ t.Fatal("SiteURL should be in envConfig")
+ }
+ }
+ })
}
func TestValidateLocales(t *testing.T) {
TranslationsPreInit()
- cfg, _, err := LoadConfig("config.json")
+ cfg, _, _, err := LoadConfig("config.json")
require.Nil(t, err)
*cfg.LocalizationSettings.DefaultServerLocale = "en"
@@ -294,18 +374,6 @@ func TestGetClientConfig(t *testing.T) {
}
})
}
-
-}
-
-func TestReadConfig(t *testing.T) {
- config, err := ReadConfig(strings.NewReader(`{
- "ServiceSettings": {
- "SiteURL": "http://foo.bar"
- }
- }`), false)
- require.NoError(t, err)
-
- assert.Equal(t, "http://foo.bar", *config.ServiceSettings.SiteURL)
}
func sToP(s string) *string {
diff --git a/utils/mail_test.go b/utils/mail_test.go
index 65b89c240..6bd8e7044 100644
--- a/utils/mail_test.go
+++ b/utils/mail_test.go
@@ -13,7 +13,7 @@ import (
)
func TestMailConnectionFromConfig(t *testing.T) {
- cfg, _, err := LoadConfig("config.json")
+ cfg, _, _, err := LoadConfig("config.json")
require.Nil(t, err)
if conn, err := ConnectToSMTPServer(cfg); err != nil {
@@ -36,7 +36,7 @@ func TestMailConnectionFromConfig(t *testing.T) {
}
func TestMailConnectionAdvanced(t *testing.T) {
- cfg, _, err := LoadConfig("config.json")
+ cfg, _, _, err := LoadConfig("config.json")
require.Nil(t, err)
if conn, err := ConnectToSMTPServerAdvanced(
@@ -86,7 +86,7 @@ func TestMailConnectionAdvanced(t *testing.T) {
}
func TestSendMailUsingConfig(t *testing.T) {
- cfg, _, err := LoadConfig("config.json")
+ cfg, _, _, err := LoadConfig("config.json")
require.Nil(t, err)
T = GetUserTranslations("en")
diff --git a/vendor/github.com/spf13/viper/nohup.out b/vendor/github.com/spf13/viper/nohup.out
deleted file mode 100644
index 8973bf27b..000000000
--- a/vendor/github.com/spf13/viper/nohup.out
+++ /dev/null
@@ -1 +0,0 @@
-QProcess::start: Process is already running
diff --git a/vendor/github.com/spf13/viper/viper.go b/vendor/github.com/spf13/viper/viper.go
index ad8a03729..b9e165695 100644
--- a/vendor/github.com/spf13/viper/viper.go
+++ b/vendor/github.com/spf13/viper/viper.go
@@ -1675,6 +1675,26 @@ func (v *Viper) AllSettings() map[string]interface{} {
return m
}
+// EnvSettings returns a map[string]interface{} containing all settings set
+// through environment variables.
+func EnvSettings() map[string]interface{} { return v.EnvSettings() }
+func (v *Viper) EnvSettings() map[string]interface{} {
+ m := map[string]interface{}{}
+ // start from the list of keys, and construct the map one value at a time
+ for _, k := range v.AllKeys() {
+ value := v.getEnv(v.mergeWithEnvPrefix(k))
+ if value == "" {
+ continue
+ }
+ path := strings.Split(k, v.keyDelim)
+ lastKey := strings.ToLower(path[len(path)-1])
+ deepestMap := deepSearch(m, path[0:len(path)-1])
+ // set innermost value
+ deepestMap[lastKey] = true
+ }
+ return m
+}
+
// SetFs sets the filesystem to use to read configuration.
func SetFs(fs afero.Fs) { v.SetFs(fs) }
func (v *Viper) SetFs(fs afero.Fs) {
@@ -1720,18 +1740,14 @@ func (v *Viper) getConfigType() string {
}
func (v *Viper) getConfigFile() (string, error) {
- // if explicitly set, then use it
- if v.configFile != "" {
- return v.configFile, nil
- }
-
- cf, err := v.findConfigFile()
- if err != nil {
- return "", err
+ if v.configFile == "" {
+ cf, err := v.findConfigFile()
+ if err != nil {
+ return "", err
+ }
+ v.configFile = cf
}
-
- v.configFile = cf
- return v.getConfigFile()
+ return v.configFile, nil
}
func (v *Viper) searchInPath(in string) (filename string) {
diff --git a/web/web_test.go b/web/web_test.go
index 60122f049..09460b3b0 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -186,8 +186,11 @@ func TestCheckClientCompatability(t *testing.T) {
{"MM App 3.7.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Mattermost/3.7.1 Chrome/56.0.2924.87 Electron/1.6.11 Safari/537.36", true},
{"Franz 4.0.4", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Franz/4.0.4 Chrome/52.0.2743.82 Electron/1.3.1 Safari/537.36", true},
{"Edge 14", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393", true},
- {"Internet Explorer 11", "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko", true},
{"Internet Explorer 9", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 7.1; Trident/5.0", false},
+ {"Internet Explorer 11", "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko", true},
+ {"Internet Explorer 11 2", "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; Zoom 3.6.0; rv:11.0) like Gecko", true},
+ {"Internet Explorer 11 (Compatibility Mode) 1", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; .NET CLR 1.1.4322; InfoPath.3; Zoom 3.6.0)", false},
+ {"Internet Explorer 11 (Compatibility Mode) 2", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; Zoom 3.6.0)", false},
{"Safari 9", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Safari/604.1.38", true},
{"Safari 8", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/600.7.12 (KHTML, like Gecko) Version/8.0.7 Safari/600.7.12", false},
{"Safari Mobile", "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B137 Safari/601.1", true},