diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/admin.go | 6 | ||||
-rw-r--r-- | app/app.go | 1 | ||||
-rw-r--r-- | app/app_test.go | 2 | ||||
-rw-r--r-- | app/apptestlib.go | 14 | ||||
-rw-r--r-- | app/auto_responder.go | 70 | ||||
-rw-r--r-- | app/auto_responder_test.go | 160 | ||||
-rw-r--r-- | app/channel.go | 20 | ||||
-rw-r--r-- | app/channel_test.go | 41 | ||||
-rw-r--r-- | app/command_invite.go | 102 | ||||
-rw-r--r-- | app/command_invite_test.go | 108 | ||||
-rw-r--r-- | app/command_mute.go | 5 | ||||
-rw-r--r-- | app/command_mute_test.go | 204 | ||||
-rw-r--r-- | app/config.go | 10 | ||||
-rw-r--r-- | app/notification.go | 24 | ||||
-rw-r--r-- | app/notification_test.go | 24 | ||||
-rw-r--r-- | app/plugin_api.go | 25 | ||||
-rw-r--r-- | app/status.go | 26 |
17 files changed, 828 insertions, 14 deletions
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 462c45613..51ee37cbd 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/app_test.go b/app/app_test.go index c2841ec53..a726fc2b5 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -195,6 +195,7 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { model.PERMISSION_LIST_USERS_WITHOUT_TEAM.Id, model.PERMISSION_MANAGE_JOBS.Id, model.PERMISSION_CREATE_POST_PUBLIC.Id, + model.PERMISSION_CREATE_POST_EPHEMERAL.Id, model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, model.PERMISSION_READ_USER_ACCESS_TOKEN.Id, model.PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, @@ -359,6 +360,7 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { model.PERMISSION_LIST_USERS_WITHOUT_TEAM.Id, model.PERMISSION_MANAGE_JOBS.Id, model.PERMISSION_CREATE_POST_PUBLIC.Id, + model.PERMISSION_CREATE_POST_EPHEMERAL.Id, model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, model.PERMISSION_READ_USER_ACCESS_TOKEN.Id, model.PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, diff --git a/app/apptestlib.go b/app/apptestlib.go index 6c2273c6e..1b22831c9 100644 --- a/app/apptestlib.go +++ b/app/apptestlib.go @@ -218,6 +218,20 @@ func (me *TestHelper) createChannel(team *model.Team, channelType string) *model return channel } +func (me *TestHelper) CreateDmChannel(user *model.User) *model.Channel { + utils.DisableDebugLogForTest() + var err *model.AppError + var channel *model.Channel + if channel, err = me.App.CreateDirectChannel(me.BasicUser.Id, user.Id); err != nil { + l4g.Error(err.Error()) + l4g.Close() + time.Sleep(time.Second) + panic(err) + } + utils.EnableDebugLogForTest() + return channel +} + func (me *TestHelper) CreatePost(channel *model.Channel) *model.Post { id := model.NewId() 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_invite.go b/app/command_invite.go new file mode 100644 index 000000000..ce443bf3d --- /dev/null +++ b/app/command_invite.go @@ -0,0 +1,102 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "strings" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/mattermost-server/model" + goi18n "github.com/nicksnyder/go-i18n/i18n" +) + +type InviteProvider struct { +} + +const ( + CMD_INVITE = "invite" +) + +func init() { + RegisterCommandProvider(&InviteProvider{}) +} + +func (me *InviteProvider) GetTrigger() string { + return CMD_INVITE +} + +func (me *InviteProvider) GetCommand(a *App, T goi18n.TranslateFunc) *model.Command { + return &model.Command{ + Trigger: CMD_INVITE, + AutoComplete: true, + AutoCompleteDesc: T("api.command_invite.desc"), + AutoCompleteHint: T("api.command_invite.hint"), + DisplayName: T("api.command_invite.name"), + } +} + +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} + } + + l4g.Debug(message) + + splitMessage := strings.SplitN(message, " ", 2) + targetUsername := splitMessage[0] + targetUsername = strings.TrimPrefix(targetUsername, "@") + + var userProfile *model.User + if result := <-a.Srv.Store.User().GetByUsername(targetUsername); result.Err != nil { + l4g.Error(result.Err.Error()) + 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) + } + + var channelToJoin *model.Channel + var err *model.AppError + // User set a channel to add the invited user + if len(splitMessage) > 1 && splitMessage[1] != "" { + targetChannelName := strings.TrimPrefix(strings.TrimSpace(splitMessage[1]), "~") + + if channelToJoin, err = a.GetChannelByName(targetChannelName, args.TeamId); err != nil { + 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} + } + } + + // 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 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} + } + + 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} + } + + 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.permission.app_error", map[string]interface{}{"User": userProfile.Username, "Channel": channelToJoin.Name}), 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} + } + + 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{} +} diff --git a/app/command_invite_test.go b/app/command_invite_test.go new file mode 100644 index 000000000..c46bc4628 --- /dev/null +++ b/app/command_invite_test.go @@ -0,0 +1,108 @@ +// 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 TestInviteProvider(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + channel := th.createChannel(th.BasicTeam, model.CHANNEL_OPEN) + privateChannel := th.createChannel(th.BasicTeam, model.CHANNEL_PRIVATE) + dmChannel := th.CreateDmChannel(th.BasicUser2) + + basicUser3 := th.CreateUser() + th.LinkUserToTeam(basicUser3, th.BasicTeam) + basicUser4 := th.CreateUser() + + InviteP := InviteProvider{} + args := &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + ChannelId: th.BasicChannel.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}}}, + } + + userAndWrongChannel := "@" + th.BasicUser2.Username + " wrongchannel1" + userAndChannel := "@" + th.BasicUser2.Username + " ~" + channel.Name + " " + userAndDisplayChannel := "@" + th.BasicUser2.Username + " ~" + channel.DisplayName + " " + userAndPrivateChannel := "@" + th.BasicUser2.Username + " ~" + privateChannel.Name + userAndDMChannel := "@" + basicUser3.Username + " ~" + dmChannel.Name + + tests := []struct { + desc string + expected string + msg string + }{ + { + desc: "Missing user and channel in the command", + expected: "api.command_invite.missing_message.app_error", + msg: "", + }, + { + desc: "User added in the current channel", + expected: "", + msg: th.BasicUser2.Username, + }, + { + desc: "Add user to another channel not the current", + expected: "api.command_invite.success", + msg: userAndChannel, + }, + { + 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 invalid channel", + expected: "api.command_invite.channel.error", + msg: userAndWrongChannel, + }, + { + desc: "Try to add a user to an private channel", + expected: "api.command_invite.success", + msg: userAndPrivateChannel, + }, + { + desc: "Using display channel name which is different form Channel name", + expected: "api.command_invite.channel.error", + msg: userAndDisplayChannel, + }, + { + desc: "Invalid user to current channel", + expected: "api.command_invite.missing_user.app_error", + msg: "@invalidUser123", + }, + { + desc: "Invalid user to current channel without @", + expected: "api.command_invite.missing_user.app_error", + msg: "invalidUser321", + }, + { + desc: "try to add a user which is not part of the team", + expected: "api.command_invite.fail.app_error", + msg: basicUser4.Username, + }, + { + desc: "try to add a user to a direct channel", + expected: "api.command_invite.directchannel.app_error", + msg: userAndDMChannel, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + actual := InviteP.DoCommand(th.App, args, test.msg).Text + assert.Equal(t, test.expected, actual) + }) + } +} 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) |