mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
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:
committed by
Joram Wilander
parent
8df6d5cc30
commit
7826774a14
@@ -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)
|
||||
|
||||
@@ -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
70
app/auto_responder.go
Normal 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
160
app/auto_responder_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"MaxNotificationsPerChannel": 1000,
|
||||
"EnableConfirmNotificationsToChannel": true,
|
||||
"TeammateNameDisplay": "username",
|
||||
"ExperimentalEnableAutomaticReplies": false,
|
||||
"ExperimentalTownSquareIsReadOnly": false,
|
||||
"ExperimentalPrimaryTeam": ""
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
STATUS_OUT_OF_OFFICE = "ooo"
|
||||
STATUS_OFFLINE = "offline"
|
||||
STATUS_AWAY = "away"
|
||||
STATUS_DND = "dnd"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user