summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api4/status.go5
-rw-r--r--api4/user.go8
-rw-r--r--app/auto_responder.go70
-rw-r--r--app/auto_responder_test.go160
-rw-r--r--app/notification.go18
-rw-r--r--app/status.go26
-rw-r--r--config/default.json1
-rw-r--r--model/config.go5
-rw-r--r--model/post.go2
-rw-r--r--model/status.go1
-rw-r--r--utils/config.go1
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)