diff options
author | Stan Chan <stanchan@users.noreply.github.com> | 2018-04-12 12:02:36 -0700 |
---|---|---|
committer | Joram Wilander <jwawilander@gmail.com> | 2018-04-12 15:02:36 -0400 |
commit | 7826774a14ebf9a3bab11069a8f2ff8947fe931a (patch) | |
tree | 8159c364cb70a640cf737e1abe24751b58b741cc | |
parent | 8df6d5cc30d22a608c58635449047e421add82ae (diff) | |
download | chat-7826774a14ebf9a3bab11069a8f2ff8947fe931a.tar.gz chat-7826774a14ebf9a3bab11069a8f2ff8947fe931a.tar.bz2 chat-7826774a14ebf9a3bab11069a8f2ff8947fe931a.zip |
Add Auto Responder handler (#8386)
WIP Out Of Office
Return error for status command if user status is OOO
Ignore notifications if Out Of Office
Disable AutoResponder if status is set to online
Add test for AutoResponder
DisableAutoResponse when manually setting status
Remove check on status slash command
return early if user does not exists in SendAutoResponse method
Add proper error handling
Add a newline after error handling
Revert back to err == nil in api4/status.go
Remove a.Go when using a.Publish
Add name consistency with the feature auto responder
Last changes for name consistency, also fix failing test with auto_responder
Fix names of functions in auto responder test
Add ExperimentalEnableAutomaticReplies flag
Auto Responder reply to a post
-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) |