[MM-8497] Ability to set Do Not Disturb for a specified period of time (#17680)
* Revert "Revert "[MM-8497] Ability to set Do Not Disturb for a specified period of time (#16067)" (#17657)"
This reverts commit ff383990f8.
* add debug log for recurring function
* add feature flag for dnd timed status
* refactoring changes
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
@@ -575,3 +575,7 @@ func (a *App) CheckIntegrity() <-chan model.IntegrityCheckResult {
|
||||
func (a *App) SetServer(srv *Server) {
|
||||
a.srv = srv
|
||||
}
|
||||
|
||||
func (a *App) UpdateExpiredDNDStatuses() ([]*model.Status, error) {
|
||||
return a.Srv().Store.Status().UpdateExpiredDNDStatuses()
|
||||
}
|
||||
|
||||
@@ -307,6 +307,9 @@ type AppIface interface {
|
||||
// relative to either the session creation date or the current time, depending
|
||||
// on the `ExtendSessionOnActivity` config setting.
|
||||
SetSessionExpireInDays(session *model.Session, days int)
|
||||
// SetStatusDoNotDisturbTimed takes endtime in unix epoch format in UTC
|
||||
// and sets status of given userId to dnd which will be restored back after endtime
|
||||
SetStatusDoNotDisturbTimed(userId string, endtime int64)
|
||||
// SetStatusLastActivityAt sets the last activity at for a user on the local app server and updates
|
||||
// status to away if needed. Used by the WS to set status to away if an 'online' device disconnects
|
||||
// while an 'away' device is still connected
|
||||
@@ -355,6 +358,9 @@ type AppIface interface {
|
||||
UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError)
|
||||
// UpdateChannelScheme saves the new SchemeId of the channel passed.
|
||||
UpdateChannelScheme(channel *model.Channel) (*model.Channel, *model.AppError)
|
||||
// UpdateDNDStatusOfUsers is a recurring task which is started when server starts
|
||||
// which unsets dnd status of users if needed and saves and broadcasts it
|
||||
UpdateDNDStatusOfUsers()
|
||||
// UpdateProductNotices is called periodically from a scheduled worker to fetch new notices and update the cache
|
||||
UpdateProductNotices() *model.AppError
|
||||
// UpdateViewedProductNotices is called from the frontend to mark a set of notices as 'viewed' by user
|
||||
@@ -1044,6 +1050,7 @@ type AppIface interface {
|
||||
UpdateCommand(oldCmd, updatedCmd *model.Command) (*model.Command, *model.AppError)
|
||||
UpdateConfig(f func(*model.Config))
|
||||
UpdateEphemeralPost(userID string, post *model.Post) *model.Post
|
||||
UpdateExpiredDNDStatuses() ([]*model.Status, error)
|
||||
UpdateGroup(group *model.Group) (*model.Group, *model.AppError)
|
||||
UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError)
|
||||
UpdateHashedPassword(user *model.User, newHashedPassword string) *model.AppError
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/app/request"
|
||||
@@ -161,8 +162,14 @@ func SetupWithoutPreloadMigrations(tb testing.TB) *TestHelper {
|
||||
func SetupWithStoreMock(tb testing.TB) *TestHelper {
|
||||
mockStore := testlib.GetMockStoreForSetupFunctions()
|
||||
th := setupTestHelper(mockStore, false, false, tb)
|
||||
statusMock := mocks.StatusStore{}
|
||||
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
|
||||
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.STATUS_ONLINE}, nil)
|
||||
statusMock.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil)
|
||||
statusMock.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil)
|
||||
emptyMockStore := mocks.Store{}
|
||||
emptyMockStore.On("Close").Return(nil)
|
||||
emptyMockStore.On("Status").Return(&statusMock)
|
||||
th.App.Srv().Store = &emptyMockStore
|
||||
return th
|
||||
}
|
||||
@@ -170,8 +177,14 @@ func SetupWithStoreMock(tb testing.TB) *TestHelper {
|
||||
func SetupEnterpriseWithStoreMock(tb testing.TB) *TestHelper {
|
||||
mockStore := testlib.GetMockStoreForSetupFunctions()
|
||||
th := setupTestHelper(mockStore, true, false, tb)
|
||||
statusMock := mocks.StatusStore{}
|
||||
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
|
||||
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.STATUS_ONLINE}, nil)
|
||||
statusMock.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil)
|
||||
statusMock.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil)
|
||||
emptyMockStore := mocks.Store{}
|
||||
emptyMockStore.On("Close").Return(nil)
|
||||
emptyMockStore.On("Status").Return(&statusMock)
|
||||
th.App.Srv().Store = &emptyMockStore
|
||||
return th
|
||||
}
|
||||
|
||||
@@ -15005,6 +15005,21 @@ func (a *OpenTracingAppLayer) SetStatusDoNotDisturb(userID string) {
|
||||
a.app.SetStatusDoNotDisturb(userID)
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) SetStatusDoNotDisturbTimed(userId string, endtime int64) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetStatusDoNotDisturbTimed")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store.SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
a.app.SetStatusDoNotDisturbTimed(userId, endtime)
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) SetStatusLastActivityAt(userID string, activityAt int64) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetStatusLastActivityAt")
|
||||
@@ -15930,6 +15945,21 @@ func (a *OpenTracingAppLayer) UpdateConfig(f func(*model.Config)) {
|
||||
a.app.UpdateConfig(f)
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateDNDStatusOfUsers() {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateDNDStatusOfUsers")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store.SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
a.app.UpdateDNDStatusOfUsers()
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateEphemeralPost(userID string, post *model.Post) *model.Post {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateEphemeralPost")
|
||||
@@ -15947,6 +15977,28 @@ func (a *OpenTracingAppLayer) UpdateEphemeralPost(userID string, post *model.Pos
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateExpiredDNDStatuses() ([]*model.Status, error) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateExpiredDNDStatuses")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store.SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.UpdateExpiredDNDStatuses()
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateGroup(group *model.Group) (*model.Group, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateGroup")
|
||||
|
||||
@@ -317,6 +317,14 @@ func (api *PluginAPI) UpdateUserStatus(userID, status string) (*model.Status, *m
|
||||
return api.app.GetStatus(userID)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) SetUserStatusTimedDND(userID string, endTime int64) (*model.Status, *model.AppError) {
|
||||
// read-after-write bug which will fail if there are replicas.
|
||||
// it works for now because we have a cache in between.
|
||||
// FIXME: make SetStatusDoNotDisturbTimed return updated status
|
||||
api.app.SetStatusDoNotDisturbTimed(userID, endTime)
|
||||
return api.app.GetStatus(userID)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetUsersInChannel(channelID, sortBy string, page, perPage int) ([]*model.User, *model.AppError) {
|
||||
switch sortBy {
|
||||
case model.CHANNEL_SORT_BY_USERNAME:
|
||||
|
||||
@@ -209,6 +209,9 @@ type Server struct {
|
||||
|
||||
imgDecoder *imaging.Decoder
|
||||
imgEncoder *imaging.Encoder
|
||||
|
||||
dndTaskMut sync.Mutex
|
||||
dndTask *model.ScheduledTask
|
||||
}
|
||||
|
||||
func NewServer(options ...Option) (*Server, error) {
|
||||
@@ -673,6 +676,7 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
s.runLicenseExpirationCheckJob()
|
||||
runCheckAdminSupportStatusJob(app, c)
|
||||
runCheckWarnMetricStatusJob(app, c)
|
||||
runDNDStatusExpireJob(app)
|
||||
})
|
||||
s.runJobs()
|
||||
}
|
||||
@@ -689,6 +693,14 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
s.ShutDownPlugins()
|
||||
}
|
||||
})
|
||||
s.AddConfigListener(func(oldCfg, newCfg *model.Config) {
|
||||
if !oldCfg.FeatureFlags.TimedDND && newCfg.FeatureFlags.TimedDND {
|
||||
runDNDStatusExpireJob(app)
|
||||
}
|
||||
if oldCfg.FeatureFlags.TimedDND && !newCfg.FeatureFlags.TimedDND {
|
||||
stopDNDStatusExpireJob(app)
|
||||
}
|
||||
})
|
||||
|
||||
return s, nil
|
||||
}
|
||||
@@ -1051,6 +1063,12 @@ func (s *Server) Shutdown() {
|
||||
mlog.Warn("Error flushing logs", mlog.Err(err))
|
||||
}
|
||||
|
||||
s.dndTaskMut.Lock()
|
||||
if s.dndTask != nil {
|
||||
s.dndTask.Cancel()
|
||||
}
|
||||
s.dndTaskMut.Unlock()
|
||||
|
||||
mlog.Info("Server stopped")
|
||||
|
||||
// this should just write the "server stopped" record, the rest are already flushed.
|
||||
@@ -2284,3 +2302,41 @@ func (s *Server) ReadFile(path string) ([]byte, *model.AppError) {
|
||||
// }
|
||||
// return result, nil
|
||||
// }
|
||||
|
||||
func createDNDStatusExpirationRecurringTask(a *App) {
|
||||
a.srv.dndTaskMut.Lock()
|
||||
a.srv.dndTask = model.CreateRecurringTaskFromNextIntervalTime("Unset DND Statuses", a.UpdateDNDStatusOfUsers, 5*time.Minute)
|
||||
a.srv.dndTaskMut.Unlock()
|
||||
}
|
||||
|
||||
func cancelDNDStatusExpirationRecurringTask(a *App) {
|
||||
a.srv.dndTaskMut.Lock()
|
||||
if a.srv.dndTask != nil {
|
||||
a.srv.dndTask.Cancel()
|
||||
a.srv.dndTask = nil
|
||||
}
|
||||
a.srv.dndTaskMut.Unlock()
|
||||
}
|
||||
|
||||
func runDNDStatusExpireJob(a *App) {
|
||||
if !a.Config().FeatureFlags.TimedDND {
|
||||
return
|
||||
}
|
||||
if a.IsLeader() {
|
||||
createDNDStatusExpirationRecurringTask(a)
|
||||
}
|
||||
a.srv.AddClusterLeaderChangedListener(func() {
|
||||
mlog.Info("Cluster leader changed. Determining if unset DNS status task should be running", mlog.Bool("isLeader", a.IsLeader()))
|
||||
if a.IsLeader() {
|
||||
createDNDStatusExpirationRecurringTask(a)
|
||||
} else {
|
||||
cancelDNDStatusExpirationRecurringTask(a)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func stopDNDStatusExpireJob(a *App) {
|
||||
if a.IsLeader() {
|
||||
cancelDNDStatusExpirationRecurringTask(a)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,6 +282,28 @@ func (a *App) SetStatusAwayIfNeeded(userID string, manual bool) {
|
||||
a.SaveAndBroadcastStatus(status)
|
||||
}
|
||||
|
||||
// SetStatusDoNotDisturbTimed takes endtime in unix epoch format in UTC
|
||||
// and sets status of given userId to dnd which will be restored back after endtime
|
||||
func (a *App) SetStatusDoNotDisturbTimed(userId string, endtime int64) {
|
||||
if !*a.Config().ServiceSettings.EnableUserStatuses {
|
||||
return
|
||||
}
|
||||
|
||||
status, err := a.GetStatus(userId)
|
||||
|
||||
if err != nil {
|
||||
status = &model.Status{UserId: userId, Status: model.STATUS_OFFLINE, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
|
||||
}
|
||||
|
||||
status.PrevStatus = status.Status
|
||||
status.Status = model.STATUS_DND
|
||||
status.Manual = true
|
||||
|
||||
status.DNDEndTime = endtime
|
||||
|
||||
a.SaveAndBroadcastStatus(status)
|
||||
}
|
||||
|
||||
func (a *App) SetStatusDoNotDisturb(userID string) {
|
||||
if !*a.Config().ServiceSettings.EnableUserStatuses {
|
||||
return
|
||||
@@ -365,6 +387,21 @@ func (a *App) IsUserAway(lastActivityAt int64) bool {
|
||||
return model.GetMillis()-lastActivityAt >= *a.Config().TeamSettings.UserStatusAwayTimeout*1000
|
||||
}
|
||||
|
||||
// UpdateDNDStatusOfUsers is a recurring task which is started when server starts
|
||||
// which unsets dnd status of users if needed and saves and broadcasts it
|
||||
func (a *App) UpdateDNDStatusOfUsers() {
|
||||
mlog.Debug("UpdateDNDStatusOfUsers: scheduled run started")
|
||||
statuses, err := a.UpdateExpiredDNDStatuses()
|
||||
if err != nil {
|
||||
mlog.Warn("Failed to fetch dnd statues from store", mlog.String("err", err.Error()))
|
||||
return
|
||||
}
|
||||
for i := range statuses {
|
||||
a.AddStatusCache(statuses[i])
|
||||
a.BroadcastStatus(statuses[i])
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) SetCustomStatus(userID string, cs *model.CustomStatus) *model.AppError {
|
||||
user, err := a.GetUser(userID)
|
||||
if err != nil {
|
||||
|
||||
Ссылка в новой задаче
Block a user