[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>
Этот коммит содержится в:
darkLord19
2021-06-17 00:08:26 +05:30
коммит произвёл GitHub
родитель 6483abd263
Коммит b0bdd23d2c
27 изменённых файлов: 615 добавлений и 21 удалений

Просмотреть файл

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