diff options
-rw-r--r-- | api4/status.go | 5 | ||||
-rw-r--r-- | api4/user.go | 8 | ||||
-rw-r--r-- | app/auto_responder.go | 70 | ||||
-rw-r--r-- | app/auto_responder_test.go | 160 | ||||
-rw-r--r-- | app/notification.go | 18 | ||||
-rw-r--r-- | app/status.go | 26 | ||||
-rw-r--r-- | config/default.json | 1 | ||||
-rw-r--r-- | model/config.go | 5 | ||||
-rw-r--r-- | model/post.go | 2 | ||||
-rw-r--r-- | model/status.go | 1 | ||||
-rw-r--r-- | utils/config.go | 1 |
11 files changed, 293 insertions, 4 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/user.go b/api4/user.go index 8f8f08c75..20b035f1d 100644 --- a/api4/user.go +++ b/api4/user.go @@ -589,8 +589,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 +612,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/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/notification.go b/app/notification.go index 06c1c19bc..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) @@ -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/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/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/model/config.go b/model/config.go index 6c6cf90e9..8d1a61926 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) } 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/utils/config.go b/utils/config.go index d0a6a17ed..7032dbad9 100644 --- a/utils/config.go +++ b/utils/config.go @@ -515,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) |