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
This commit is contained in:
Stan Chan
2018-04-12 12:02:36 -07:00
committed by Joram Wilander
parent 8df6d5cc30
commit 7826774a14
11 changed files with 293 additions and 4 deletions

View File

@@ -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)

View File

@@ -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()))
}

70
app/auto_responder.go Normal file
View File

@@ -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
}

160
app/auto_responder_test.go Normal file
View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -89,6 +89,7 @@
"MaxNotificationsPerChannel": 1000,
"EnableConfirmNotificationsToChannel": true,
"TeammateNameDisplay": "username",
"ExperimentalEnableAutomaticReplies": false,
"ExperimentalTownSquareIsReadOnly": false,
"ExperimentalPrimaryTeam": ""
},

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -9,6 +9,7 @@ import (
)
const (
STATUS_OUT_OF_OFFICE = "ooo"
STATUS_OFFLINE = "offline"
STATUS_AWAY = "away"
STATUS_DND = "dnd"

View File

@@ -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)