[MM-47751][MM-48102] MPA: Send Persistent Notifications (#21619)

* MM-46410: adds urgency on mention counts

We have introduced priority for posts in
https://github.com/mattermost/mattermost-webapp/pull/10951.
We do need to color the mention badges in the webapp with a prominent
color when a mention is posted in an urgent message.
A thread has urgent mentions if the root post is marked as urgent, and
the replies contain mentions to the user viewing the thread.

This PR adds two columns, urgentmentioncount, and isurgent, in
channelmembers, and threads tables respectively.
Furthermore when asking for team/thread mention counts, we also return
urgent mention counts for the user.

* Adds PostAcknowledgements table and apis

* job init and fetch mentions

* add-migrations

* delete-expired

* send-notifications

* Fetches post priority in batches

* stop-notifications

* stop-notification-on-reply

* MM-47750: Adds PostAcknowledgements table and apis

- Adds post acknowledgement api/app/store methods to be able to save and
delete post acknowledgements by users.
- Adds wesbsocket events for acknowledgement created/deleted
- Returns post acknowledgements in the post's metadata

* add-license-check

* add-pagination

* delete on channel and team

* validate guests

* add configs

* move create priority post check from app to api

* Add desktop notifications

* check status

* use config in job

* add IsUrgent check

* Add last-sent-at

* validate max recipients

* Update lastSentAt

* Validate min. recipient

* send email notification only once

* remove email notifications

* use latest time from config to run job

* Add notifications counter

* publish events to mentioned users only

* pickup license updates in scheduler

* don't allow post owner to stop notifications

* follow normal notifications behaviour

* Validates persistent notifications interval

* move logic of handling valid and expired posts into sql

* Adds persistent notifications in the webapp

---------

Co-authored-by: koox00 <3829551+koox00@users.noreply.github.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Vishal
2023-05-18 23:44:12 +05:30
коммит произвёл GitHub
родитель ce165302cf
Коммит 9399ce8637
74 изменённых файлов: 4052 добавлений и 888 удалений

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

@@ -80,6 +80,57 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
post.CreateAt = 0
}
if post.GetPriority() != nil {
priorityForbiddenErr := model.NewAppError("Api4.createPost", "api.post.post_priority.priority_post_not_allowed_for_user.request_error", nil, "userId="+c.AppContext.Session().UserId, http.StatusForbidden)
if !c.App.IsPostPriorityEnabled() {
c.Err = priorityForbiddenErr
return
}
if post.RootId != "" {
c.Err = model.NewAppError("Api4.createPost", "api.post.post_priority.priority_post_only_allowed_for_root_post.request_error", nil, "", http.StatusBadRequest)
return
}
if ack := post.GetRequestedAck(); ack != nil && *ack {
licenseErr := minimumProfessionalLicense(c)
if licenseErr != nil {
c.Err = licenseErr
return
}
}
if notification := post.GetPersistentNotification(); notification != nil && *notification {
licenseErr := minimumProfessionalLicense(c)
if licenseErr != nil {
c.Err = licenseErr
return
}
if !c.App.IsPersistentNotificationsEnabled() {
c.Err = priorityForbiddenErr
return
}
if !post.IsUrgent() {
c.Err = model.NewAppError("Api4.createPost", "api.post.post_priority.urgent_persistent_notification_post.request_error", nil, "", http.StatusBadRequest)
return
}
if !*c.App.Config().ServiceSettings.AllowPersistentNotificationsForGuests {
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if user.IsGuest() {
c.Err = priorityForbiddenErr
return
}
}
}
}
setOnline := r.URL.Query().Get("set_online")
setOnlineBool := true // By default, always set online.
var err2 error

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

@@ -12,6 +12,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"reflect"
"sort"
"strings"
@@ -211,6 +212,178 @@ func TestCreatePost(t *testing.T) {
require.Equal(t, post.CreateAt, rpost.CreateAt, "create at should match")
}
func TestCreatePostForPriority(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY")
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostPriority = true
*cfg.ServiceSettings.AllowPersistentNotifications = true
})
t.Run("should return forbidden when post-priority is disabled", func(t *testing.T) {
originalPrioritySetting := *th.App.Config().ServiceSettings.PostPriority
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostPriority = false
})
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostPriority = originalPrioritySetting
})
post := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("urgent"),
},
}}
_, resp, err := client.CreatePost(post)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("should return badRequest when priority is set for reply post", func(t *testing.T) {
rootPost := &model.Post{ChannelId: th.BasicChannel.Id, Message: "root"}
post, resp, err := client.CreatePost(rootPost)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
replyPost := &model.Post{RootId: post.Id, ChannelId: th.BasicChannel.Id, Message: "reply", Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("urgent"),
},
}}
_, resp, err = client.CreatePost(replyPost)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("should return statusNotImplemented when min. pro. license not available", func(t *testing.T) {
th.App.Srv().RemoveLicense()
defer th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
// for Acknowledment
p1 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("urgent"),
RequestedAck: model.NewBool(true),
},
}}
_, resp, err := client.CreatePost(p1)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
// for Persistent Notification
p2 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("urgent"),
PersistentNotifications: model.NewBool(true),
},
}}
_, resp, err = client.CreatePost(p2)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
t.Run("should return forbidden when persistent notification not enabled", func(t *testing.T) {
originalSetting := *th.App.Config().ServiceSettings.AllowPersistentNotifications
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowPersistentNotifications = false
})
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowPersistentNotifications = originalSetting
})
p1 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("urgent"),
PersistentNotifications: model.NewBool(true),
},
}}
_, resp, err := client.CreatePost(p1)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("should return badRequest when post is not urgent for persistent notification", func(t *testing.T) {
p1 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
PersistentNotifications: model.NewBool(true),
},
}}
_, resp, err := client.CreatePost(p1)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("should return forbidden when persistent notification is disabled for guest users", func(t *testing.T) {
originalSetting := *th.App.Config().ServiceSettings.AllowPersistentNotificationsForGuests
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowPersistentNotificationsForGuests = false
})
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowPersistentNotificationsForGuests = originalSetting
})
appErr := th.App.DemoteUserToGuest(th.Context, th.BasicUser)
require.Nil(t, appErr)
defer th.App.PromoteGuestToUser(th.Context, th.BasicUser, th.SystemAdminUser.Id)
p1 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("urgent"),
PersistentNotifications: model.NewBool(true),
},
}}
_, resp, err := client.CreatePost(p1)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("should create priority post", func(t *testing.T) {
p1 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
},
}}
_, resp, err := client.CreatePost(p1)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
})
t.Run("should create acknowledge post", func(t *testing.T) {
p1 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(""),
RequestedAck: model.NewBool(true),
},
}}
_, resp, err := client.CreatePost(p1)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
})
t.Run("should create persistent notification post", func(t *testing.T) {
p1 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test @" + th.BasicUser2.Username, Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("urgent"),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(true),
},
}}
_, resp, err := client.CreatePost(p1)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
})
}
func TestCreatePostWithOAuthClient(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()

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

@@ -5991,6 +5991,9 @@ func TestGetThreadsForUser(t *testing.T) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
})
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
t.Run("empty", func(t *testing.T) {
client := th.Client
@@ -6084,47 +6087,84 @@ func TestGetThreadsForUser(t *testing.T) {
require.Greater(t, uss.Threads[0].Post.DeleteAt, int64(0))
})
t.Run("throw error when post-priority service-setting is off", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostPriority = false
cfg.FeatureFlags.PostPriority = true
})
client := th.Client
_, resp, err := client.CreatePost(&model.Post{
ChannelId: th.BasicChannel.Id,
Message: "testMsg",
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
},
},
})
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("throw error when post-priority is set for a reply", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostPriority = true
cfg.FeatureFlags.PostPriority = true
})
client := th.Client
defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.BasicUser.Id)
rpost, resp, err := client.CreatePost(&model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, resp, err = client.CreatePost(&model.Post{
RootId: rpost.Id,
ChannelId: th.BasicChannel.Id,
Message: "testReply",
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
},
},
})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("isUrgent, 1 thread", func(t *testing.T) {
testCases := []struct {
featureEnabled bool
expected bool
}{
{featureEnabled: true, expected: true},
{featureEnabled: false, expected: false},
}
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostPriority = true
cfg.FeatureFlags.PostPriority = true
})
for _, tc := range testCases {
func() {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostPriority = tc.featureEnabled
cfg.FeatureFlags.PostPriority = true
})
client := th.Client
client := th.Client
rpost, resp, err := client.CreatePost(&model.Post{
ChannelId: th.BasicChannel.Id,
Message: "testMsg",
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
},
},
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, resp, err = client.CreatePost(&model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
rpost, resp, err := client.CreatePost(&model.Post{
ChannelId: th.BasicChannel.Id,
Message: "testMsg",
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
},
},
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, resp, err = client.CreatePost(&model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.BasicUser.Id)
defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.BasicUser.Id)
uss, _, err := th.Client.GetUserThreads(th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
require.NoError(t, err)
require.Len(t, uss.Threads, 1)
require.Equal(t, uss.Threads[0].IsUrgent, tc.expected)
}()
}
uss, _, err := th.Client.GetUserThreads(th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
require.NoError(t, err)
require.Len(t, uss.Threads, 1)
require.Equal(t, true, uss.Threads[0].IsUrgent)
})
t.Run("paged, 30 threads", func(t *testing.T) {
@@ -6864,9 +6904,10 @@ func TestSingleThreadGet(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.PostPriority = false
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
*cfg.ServiceSettings.PostPriority = true
cfg.FeatureFlags.PostPriority = true

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

@@ -122,6 +122,8 @@ type AppIface interface {
// DeleteGroupConstrainedMemberships deletes team and channel memberships of users who aren't members of the allowed
// groups of all group-constrained teams and channels.
DeleteGroupConstrainedMemberships(c *request.Context) error
// DeletePersistentNotification stops the persistent notifications.
DeletePersistentNotification(c request.CTX, post *model.Post) *model.AppError
// DeletePublicKey will delete plugin public key from the config.
DeletePublicKey(name string) *model.AppError
// DemoteUserToGuest Convert user's roles and all his membership's roles from
@@ -297,6 +299,9 @@ type AppIface interface {
RenameChannel(c request.CTX, channel *model.Channel, newChannelName string, newDisplayName string) (*model.Channel, *model.AppError)
// RenameTeam is used to rename the team Name and the DisplayName fields
RenameTeam(team *model.Team, newTeamName string, newDisplayName string) (*model.Team, *model.AppError)
// ResolvePersistentNotification stops the persistent notifications, if a loggedInUserID(except the post owner) reacts, reply or ack on the post.
// Post-owner can only delete the original post to stop the notifications.
ResolvePersistentNotification(c request.CTX, post *model.Post, loggedInUserID string) *model.AppError
// RevokeSessionsFromAllUsers will go through all the sessions active
// in the server and revoke them
RevokeSessionsFromAllUsers() *model.AppError
@@ -898,8 +903,10 @@ type AppIface interface {
IsFirstUserAccount() bool
IsLeader() bool
IsPasswordValid(password string) *model.AppError
IsPersistentNotificationsEnabled() bool
IsPhase2MigrationCompleted() *model.AppError
IsPluginActive(pluginName string) (bool, error)
IsPostPriorityEnabled() bool
IsUserSignUpAllowed() *model.AppError
JoinChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError
JoinDefaultChannels(c request.CTX, teamID string, user *model.User, shouldBeAdmin bool, userRequestorId string) *model.AppError
@@ -1053,6 +1060,7 @@ type AppIface interface {
SendNotifyAdminPosts(c *request.Context, workspaceName string, currentSKU string, trial bool) *model.AppError
SendPasswordReset(email string, siteURL string) (bool, *model.AppError)
SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model.AppError
SendPersistentNotifications() error
SendTestPushNotification(deviceID string) string
SendUpgradeConfirmationEmail(isYearly bool) *model.AppError
ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId, destinationPluginId string)

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

@@ -1521,11 +1521,16 @@ func (a *App) DeleteChannel(c request.CTX, channel *model.Channel, userID string
}
}
if err := a.Srv().Store().PostPersistentNotification().DeleteByChannel([]string{channel.Id}); err != nil {
return model.NewAppError("DeleteChannel", "app.post_persistent_notification.delete_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
deleteAt := model.GetMillis()
if err := a.Srv().Store().Channel().Delete(channel.Id, deleteAt); err != nil {
return model.NewAppError("DeleteChannel", "app.channel.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.Srv().Platform().InvalidateCacheForChannel(channel)
message := model.NewWebSocketEvent(model.WebsocketEventChannelDeleted, channel.TeamId, "", "", nil, "")
@@ -2791,7 +2796,7 @@ func (a *App) markChannelAsUnreadFromPostCRTUnsupported(c request.CTX, postID st
if mErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
}
thread, mErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.isPostPriorityEnabled())
thread, mErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.IsPostPriorityEnabled())
if mErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
}
@@ -3095,6 +3100,10 @@ func (a *App) PermanentDeleteChannel(c request.CTX, channel *model.Channel) *mod
return model.NewAppError("PermanentDeleteChannel", "app.webhooks.permanent_delete_outgoing_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().PostPersistentNotification().DeleteByChannel([]string{channel.Id}); err != nil {
return model.NewAppError("PermanentDeleteChannel", "app.post_persistent_notification.delete_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
deleteAt := model.GetMillis()
if nErr := a.Srv().Store().Channel().PermanentDelete(channel.Id); nErr != nil {

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

@@ -607,7 +607,7 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
}
threadMembership = tm
}
userThread, err := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.isPostPriorityEnabled())
userThread, err := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.IsPostPriorityEnabled())
if err != nil {
return nil, errors.Wrapf(err, "cannot get thread %q for user %q", post.RootId, uid)
}

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

@@ -3285,6 +3285,28 @@ func (a *OpenTracingAppLayer) DeleteOutgoingWebhook(hookID string) *model.AppErr
return resultVar0
}
func (a *OpenTracingAppLayer) DeletePersistentNotification(c request.CTX, post *model.Post) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeletePersistentNotification")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeletePersistentNotification(c, post)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeletePluginKey(pluginID string, key string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeletePluginKey")
@@ -12121,6 +12143,23 @@ func (a *OpenTracingAppLayer) IsPasswordValid(password string) *model.AppError {
return resultVar0
}
func (a *OpenTracingAppLayer) IsPersistentNotificationsEnabled() bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsPersistentNotificationsEnabled")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsPersistentNotificationsEnabled()
return resultVar0
}
func (a *OpenTracingAppLayer) IsPhase2MigrationCompleted() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsPhase2MigrationCompleted")
@@ -12165,6 +12204,23 @@ func (a *OpenTracingAppLayer) IsPluginActive(pluginName string) (bool, error) {
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) IsPostPriorityEnabled() bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsPostPriorityEnabled")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsPostPriorityEnabled()
return resultVar0
}
func (a *OpenTracingAppLayer) IsUserSignUpAllowed() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsUserSignUpAllowed")
@@ -14397,6 +14453,28 @@ func (a *OpenTracingAppLayer) ResetSamlAuthDataToEmail(includeDeleted bool, dryR
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ResolvePersistentNotification(c request.CTX, post *model.Post, loggedInUserID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ResolvePersistentNotification")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ResolvePersistentNotification(c, post, loggedInUserID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RestoreChannel(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RestoreChannel")
@@ -15761,6 +15839,28 @@ func (a *OpenTracingAppLayer) SendPaymentFailedEmail(failedPayment *model.Failed
return resultVar0
}
func (a *OpenTracingAppLayer) SendPersistentNotifications() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendPersistentNotifications")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendPersistentNotifications()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SendSubscriptionHistoryEvent(userID string) (*model.SubscriptionHistory, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendSubscriptionHistoryEvent")

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

@@ -196,6 +196,21 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel
a.Srv().seenPendingPostIdsCache.SetWithExpiry(post.PendingPostId, savedPost.Id, PendingPostIDsCacheTTL)
}()
// Validate recipients counts in case it's not DM
if persistentNotification := post.GetPersistentNotification(); persistentNotification != nil && *persistentNotification && channel.Type != model.ChannelTypeDirect {
err := a.forEachPersistentNotificationPost([]*model.Post{post}, func(_ *model.Post, _ *model.Channel, _ *model.Team, mentions *ExplicitMentions, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
if maxRecipients := *a.Config().ServiceSettings.PersistentNotificationMaxRecipients; len(mentions.Mentions) > maxRecipients {
return model.NewAppError("CreatePost", "api.post.post_priority.max_recipients_persistent_notification_post.request_error", map[string]any{"MaxRecipients": maxRecipients}, "", http.StatusBadRequest)
} else if len(mentions.Mentions) == 0 {
return model.NewAppError("CreatePost", "api.post.post_priority.min_recipients_persistent_notification_post.request_error", nil, "", http.StatusBadRequest)
}
return nil
})
if err != nil {
return nil, model.NewAppError("CreatePost", "api.post.post_priority.persistent_notification_validation_error.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
post.SanitizeProps()
var pchan chan store.StoreResult
@@ -279,10 +294,6 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel
}
}
if !a.isPostPriorityEnabled() && post.GetPriority() != nil {
post.Metadata.Priority = nil
}
var metadata *model.PostMetadata
if post.Metadata != nil {
metadata = post.Metadata.Copy()
@@ -374,6 +385,12 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel
// so we just return the one that was passed with post
rpost = a.PreparePostForClient(c, rpost, true, false, false)
if rpost.RootId != "" {
if appErr := a.ResolvePersistentNotification(c, parentPostList.Posts[post.RootId], rpost.UserId); appErr != nil {
return nil, appErr
}
}
// Make sure poster is following the thread
if *a.Config().ServiceSettings.ThreadAutoFollow && rpost.RootId != "" {
_, err := a.Srv().Store().Thread().MaintainMembership(user.Id, rpost.RootId, store.ThreadMembershipOpts{
@@ -1288,6 +1305,12 @@ func (a *App) DeletePost(c request.CTX, postID, deleteByID string) (*model.Post,
}
}
if post.RootId == "" {
if appErr := a.DeletePersistentNotification(c, post); appErr != nil {
return nil, appErr
}
}
postJSON, err := json.Marshal(post)
if err != nil {
return nil, model.NewAppError("DeletePost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
@@ -1792,7 +1815,7 @@ func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model
}
var urgentCount int
if a.isPostPriorityEnabled() {
if a.IsPostPriorityEnabled() {
urgentCount, nErr = a.Srv().Store().Channel().CountUrgentPostsAfter(post.ChannelId, post.CreateAt-1, channel.GetOtherUserIdForDM(user.Id))
if nErr != nil {
return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.count_urgent_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
@@ -1832,7 +1855,7 @@ func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model
count += 1
if post.RootId == "" {
countRoot += 1
if a.isPostPriorityEnabled() {
if a.IsPostPriorityEnabled() {
priority, err := a.GetPriorityForPost(post.Id)
if err != nil {
return 0, 0, 0, err
@@ -1868,7 +1891,7 @@ func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model
}
}
if a.isPostPriorityEnabled() {
if a.IsPostPriorityEnabled() {
priorityList, nErr := a.Srv().Store().PostPriority().GetForPosts(mentionPostIds)
if nErr != nil {
return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.get_priority_for_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
@@ -2277,7 +2300,3 @@ func includeEmbedsAndImages(a *App, c request.CTX, topThreadList *model.TopThrea
}
return topThreadList, nil
}
func (a *App) isPostPriorityEnabled() bool {
return a.Config().FeatureFlags.PostPriority && *a.Config().ServiceSettings.PostPriority
}

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

@@ -31,7 +31,6 @@ func (a *App) SaveAcknowledgementForPost(c *request.Context, postID, userID stri
acknowledgedAt := model.GetMillis()
acknowledgement, nErr := a.Srv().Store().PostAcknowledgement().Save(postID, userID, acknowledgedAt)
if nErr != nil {
var appErr *model.AppError
switch {
@@ -42,6 +41,10 @@ func (a *App) SaveAcknowledgementForPost(c *request.Context, postID, userID stri
}
}
if appErr := a.ResolvePersistentNotification(c, post, userID); appErr != nil {
return nil, appErr
}
// The post is always modified since the UpdateAt always changes
a.invalidateCacheForChannelPosts(channel.Id)

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

@@ -63,7 +63,7 @@ func (a *App) PreparePostListForClient(c request.CTX, originalList *model.PostLi
list.Posts[id] = post
}
if a.isPostPriorityEnabled() {
if a.IsPostPriorityEnabled() {
priority, _ := a.GetPriorityForPostList(list)
acknowledgements, _ := a.GetAcknowledgementsForPostList(list)
@@ -139,7 +139,7 @@ func (a *App) PreparePostForClient(c request.CTX, originalPost *model.Post, isNe
post.Metadata.Files = fileInfos
}
if includePriority && a.isPostPriorityEnabled() && post.RootId == "" {
if includePriority && a.IsPostPriorityEnabled() && post.RootId == "" {
// Post's Priority if any
if priority, err := a.GetPriorityForPost(post.Id); err != nil {
mlog.Warn("Failed to get post priority for a post", mlog.String("post_id", post.Id), mlog.Err(err))

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

@@ -0,0 +1,396 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"net/http"
"time"
"github.com/mattermost/mattermost-server/server/public/model"
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
"github.com/mattermost/mattermost-server/server/v8/channels/store"
"github.com/pkg/errors"
)
// ResolvePersistentNotification stops the persistent notifications, if a loggedInUserID(except the post owner) reacts, reply or ack on the post.
// Post-owner can only delete the original post to stop the notifications.
func (a *App) ResolvePersistentNotification(c request.CTX, post *model.Post, loggedInUserID string) *model.AppError {
// Ignore the post owner's actions to their own post
if loggedInUserID == post.UserId {
return nil
}
if !a.IsPersistentNotificationsEnabled() {
return nil
}
_, err := a.Srv().Store().PostPersistentNotification().GetSingle(post.Id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
// Either the notification post is already deleted or was never a notification post
return nil
default:
return model.NewAppError("ResolvePersistentNotification", "app.post_priority.delete_persistent_notification_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if !*a.Config().ServiceSettings.AllowPersistentNotificationsForGuests {
user, nErr := a.Srv().Store().User().Get(context.Background(), loggedInUserID)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("ResolvePersistentNotification", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("ResolvePersistentNotification", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if user.IsGuest() {
return nil
}
}
stopNotifications := false
if err := a.forEachPersistentNotificationPost([]*model.Post{post}, func(_ *model.Post, _ *model.Channel, _ *model.Team, mentions *ExplicitMentions, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
if mentions.isUserMentioned(loggedInUserID) {
stopNotifications = true
}
return nil
}); err != nil {
return model.NewAppError("ResolvePersistentNotification", "app.post_priority.delete_persistent_notification_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Only mentioned users can stop the notifications
if !stopNotifications {
return nil
}
if err := a.Srv().Store().PostPersistentNotification().Delete([]string{post.Id}); err != nil {
return model.NewAppError("ResolvePersistentNotification", "app.post_priority.delete_persistent_notification_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// DeletePersistentNotification stops the persistent notifications.
func (a *App) DeletePersistentNotification(c request.CTX, post *model.Post) *model.AppError {
if !a.IsPersistentNotificationsEnabled() {
return nil
}
_, err := a.Srv().Store().PostPersistentNotification().GetSingle(post.Id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
// Either the notification post is already deleted or was never a notification post
return nil
default:
return model.NewAppError("DeletePersistentNotification", "app.post_priority.delete_persistent_notification_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if err := a.Srv().Store().PostPersistentNotification().Delete([]string{post.Id}); err != nil {
return model.NewAppError("DeletePersistentNotification", "app.post_priority.delete_persistent_notification_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) SendPersistentNotifications() error {
notificationInterval := time.Duration(*a.Config().ServiceSettings.PersistentNotificationIntervalMinutes) * time.Minute
notificationMaxCount := int16(*a.Config().ServiceSettings.PersistentNotificationMaxCount)
// fetch posts for which the "notificationInterval duration" has passed
maxTime := time.Now().Add(-notificationInterval).UnixMilli()
// Pagination loop
for {
notificationPosts, err := a.Srv().Store().PostPersistentNotification().Get(model.GetPersistentNotificationsPostsParams{
MaxTime: maxTime,
MaxSentCount: notificationMaxCount,
PerPage: 500,
})
if err != nil {
return errors.Wrap(err, "failed to get posts for persistent notifications")
}
// No posts left to send persistent notifications
if len(notificationPosts) == 0 {
break
}
postIds := make([]string, 0, len(notificationPosts))
for _, p := range notificationPosts {
postIds = append(postIds, p.PostId)
}
posts, err := a.Srv().Store().Post().GetPostsByIds(postIds)
if err != nil {
return errors.Wrap(err, "failed to get posts by IDs")
}
// Send notifications
if err := a.forEachPersistentNotificationPost(posts, a.sendPersistentNotifications); err != nil {
return err
}
if err := a.Srv().Store().PostPersistentNotification().UpdateLastActivity(postIds); err != nil {
return errors.Wrapf(err, "failed to update lastActivity for notifications: %v", postIds)
}
}
if err := a.Srv().Store().PostPersistentNotification().DeleteExpired(notificationMaxCount); err != nil {
return errors.Wrap(err, "failed to delete expired notifications")
}
return nil
}
func (a *App) forEachPersistentNotificationPost(posts []*model.Post, fn func(post *model.Post, channel *model.Channel, team *model.Team, mentions *ExplicitMentions, profileMap model.UserMap, channelNotifyProps map[string]map[string]model.StringMap) error) error {
channelsMap, teamsMap, err := a.channelTeamMapsForPosts(posts)
if err != nil {
return err
}
channelGroupMap, channelProfileMap, channelKeywords, channelNotifyProps, err := a.persistentNotificationsAuxiliaryData(channelsMap, teamsMap)
if err != nil {
return err
}
for _, post := range posts {
channel := channelsMap[post.ChannelId]
team := teamsMap[channel.TeamId]
// GMs and DMs don't belong to any team
if channel.IsGroupOrDirect() {
team = &model.Team{}
}
profileMap := channelProfileMap[channel.Id]
mentions := &ExplicitMentions{}
// In DMs, only the "other" user can be mentioned
if channel.Type == model.ChannelTypeDirect {
otherUserId := channel.GetOtherUserIdForDM(post.UserId)
if _, ok := profileMap[otherUserId]; ok {
mentions.addMention(otherUserId, DMMention)
}
} else {
keywords := channelKeywords[channel.Id]
mentions = getExplicitMentions(post, keywords, channelGroupMap[channel.Id])
for _, group := range mentions.GroupMentions {
_, err := a.insertGroupMentions(group, channel, profileMap, mentions)
if err != nil {
return errors.Wrapf(err, "failed to include mentions from group - %s for channel - %s", group.Id, channel.Id)
}
}
}
if err := fn(post, channel, team, mentions, profileMap, channelNotifyProps); err != nil {
return err
}
}
return nil
}
func (a *App) persistentNotificationsAuxiliaryData(channelsMap map[string]*model.Channel, teamsMap map[string]*model.Team) (map[string]map[string]*model.Group, map[string]model.UserMap, map[string]map[string][]string, map[string]map[string]model.StringMap, error) {
channelGroupMap := make(map[string]map[string]*model.Group, len(channelsMap))
channelProfileMap := make(map[string]model.UserMap, len(channelsMap))
channelKeywords := make(map[string]map[string][]string, len(channelsMap))
channelNotifyProps := make(map[string]map[string]model.StringMap, len(channelsMap))
for _, c := range channelsMap {
// In DM, notifications can't be send to any 3rd person.
if c.Type != model.ChannelTypeDirect {
groups, err := a.getGroupsAllowedForReferenceInChannel(c, teamsMap[c.TeamId])
if err != nil {
return nil, nil, nil, nil, errors.Wrapf(err, "failed to get profiles for channel %s", c.Id)
}
channelGroupMap[c.Id] = make(map[string]*model.Group, len(groups))
for k, v := range groups {
channelGroupMap[c.Id][k] = v
}
props, err := a.Srv().Store().Channel().GetAllChannelMembersNotifyPropsForChannel(c.Id, true)
if err != nil {
return nil, nil, nil, nil, errors.Wrapf(err, "failed to get profiles for channel %s", c.Id)
}
channelNotifyProps[c.Id] = props
}
profileMap, err := a.Srv().Store().User().GetAllProfilesInChannel(context.Background(), c.Id, true)
if err != nil {
return nil, nil, nil, nil, errors.Wrapf(err, "failed to get profiles for channel %s", c.Id)
}
channelKeywords[c.Id] = make(map[string][]string, len(profileMap))
validProfileMap := make(map[string]*model.User, len(profileMap))
for k, v := range profileMap {
if v.IsBot {
continue
}
validProfileMap[k] = v
channelKeywords[c.Id]["@"+v.Username] = []string{k}
}
channelProfileMap[c.Id] = validProfileMap
}
return channelGroupMap, channelProfileMap, channelKeywords, channelNotifyProps, nil
}
func (a *App) channelTeamMapsForPosts(posts []*model.Post) (map[string]*model.Channel, map[string]*model.Team, error) {
channelIds := make(model.StringSet)
for _, p := range posts {
channelIds.Add(p.ChannelId)
}
channels, err := a.Srv().Store().Channel().GetChannelsByIds(channelIds.Val(), false)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to get teams by IDs")
}
channelsMap := make(map[string]*model.Channel, len(channels))
for _, c := range channels {
channelsMap[c.Id] = c
}
teamIds := make(model.StringSet)
for _, c := range channels {
if c.TeamId != "" {
teamIds.Add(c.TeamId)
}
}
teams := make([]*model.Team, 0, len(teamIds))
if len(teamIds) > 0 {
teams, err = a.Srv().Store().Team().GetMany(teamIds.Val())
if err != nil {
return nil, nil, errors.Wrap(err, "failed to get teams by IDs")
}
}
teamsMap := make(map[string]*model.Team, len(teams))
for _, t := range teams {
teamsMap[t.Id] = t
}
return channelsMap, teamsMap, nil
}
func (a *App) sendPersistentNotifications(post *model.Post, channel *model.Channel, team *model.Team, mentions *ExplicitMentions, profileMap model.UserMap, channelNotifyProps map[string]map[string]model.StringMap) error {
mentionedUsersList := make(model.StringArray, 0, len(mentions.Mentions))
for id := range mentions.Mentions {
// Don't send notification to post owner
if id != post.UserId {
mentionedUsersList = append(mentionedUsersList, id)
}
}
sender := profileMap[post.UserId]
notification := &PostNotification{
Post: post,
Channel: channel,
ProfileMap: profileMap,
Sender: sender,
}
if int64(len(mentionedUsersList)) > *a.Config().TeamSettings.MaxNotificationsPerChannel {
return errors.Errorf("mentioned users: %d are more than allowed users: %d", len(mentionedUsersList), *a.Config().TeamSettings.MaxNotificationsPerChannel)
}
if a.canSendPushNotifications() {
for _, userID := range mentionedUsersList {
user := profileMap[userID]
if user == nil {
continue
}
status, err := a.GetStatus(userID)
if err != nil {
mlog.Warn("Unable to fetch online status", mlog.String("user_id", userID), mlog.Err(err))
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
if ShouldSendPushNotification(profileMap[userID], channelNotifyProps[channel.Id][userID], true, status, post) {
a.sendPushNotification(
notification,
user,
true,
false,
"",
)
} else {
// register that a notification was not sent
a.NotificationsLog().Debug("Persistent Notification not sent",
mlog.String("ackId", ""),
mlog.String("type", model.PushTypeMessage),
mlog.String("userId", userID),
mlog.String("postId", post.Id),
mlog.String("status", model.PushNotSent),
)
}
}
}
desktopUsers := make([]string, 0, len(mentionedUsersList))
for _, id := range mentionedUsersList {
user := profileMap[id]
if user == nil {
continue
}
if user.NotifyProps[model.DesktopNotifyProp] != model.UserNotifyNone && a.persistentNotificationsAllowedForStatus(id) {
desktopUsers = append(desktopUsers, id)
}
}
if len(desktopUsers) != 0 {
post = a.PreparePostForClient(request.EmptyContext(a.Log()), post, false, false, true)
postJSON, jsonErr := post.ToJSON()
if jsonErr != nil {
return errors.Wrapf(jsonErr, "failed to encode post to JSON")
}
for _, u := range desktopUsers {
message := model.NewWebSocketEvent(model.WebsocketEventPersistentNotificationTriggered, team.Id, post.ChannelId, u, nil, "")
message.Add("post", postJSON)
message.Add("channel_type", channel.Type)
message.Add("channel_display_name", notification.GetChannelName(model.ShowUsername, ""))
message.Add("channel_name", channel.Name)
message.Add("sender_name", notification.GetSenderName(model.ShowUsername, *a.Config().ServiceSettings.EnablePostUsernameOverride))
message.Add("team_id", team.Id)
if len(post.FileIds) != 0 {
message.Add("otherFile", "true")
infos, err := a.Srv().Store().FileInfo().GetForPost(post.Id, false, false, true)
if err != nil {
mlog.Warn("Unable to get fileInfo for push notifications.", mlog.String("post_id", post.Id), mlog.Err(err))
}
for _, info := range infos {
if info.IsImage() {
message.Add("image", "true")
break
}
}
}
message.Add("mentions", model.ArrayToJSON(desktopUsers))
a.Publish(message)
}
}
return nil
}
func (a *App) persistentNotificationsAllowedForStatus(userID string) bool {
var status *model.Status
var err *model.AppError
if status, err = a.GetStatus(userID); err != nil {
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
return status.Status != model.StatusDnd && status.Status != model.StatusOutOfOffice
}
func (a *App) IsPersistentNotificationsEnabled() bool {
return a.IsPostPriorityEnabled() && *a.Config().ServiceSettings.AllowPersistentNotifications
}

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

@@ -0,0 +1,237 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"os"
"testing"
"github.com/mattermost/mattermost-server/server/public/model"
"github.com/mattermost/mattermost-server/server/v8/channels/store"
storemocks "github.com/mattermost/mattermost-server/server/v8/channels/store/storetest/mocks"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestResolvePersistentNotification(t *testing.T) {
t.Run("should not delete when no posts exist", func(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY")
th := SetupWithStoreMock(t)
defer th.TearDown()
post := &model.Post{Id: "test id"}
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
mockPostPersistentNotification.On("GetSingle", mock.Anything).Return(nil, &store.ErrNotFound{})
mockPostPersistentNotification.On("Delete", mock.Anything).Return(nil)
th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
cfg := th.App.Config()
*cfg.ServiceSettings.PostPriority = true
*cfg.ServiceSettings.AllowPersistentNotificationsForGuests = true
err := th.App.ResolvePersistentNotification(th.Context, post, "")
require.Nil(t, err)
mockPostPersistentNotification.AssertNotCalled(t, "Delete", mock.Anything)
})
t.Run("should delete for mentioned user", func(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY")
th := SetupWithStoreMock(t)
defer th.TearDown()
user1 := &model.User{Id: "uid1", Username: "user-1"}
user2 := &model.User{Id: "uid2", Username: "user-2"}
profileMap := map[string]*model.User{user1.Id: user1, user2.Id: user2}
team := &model.Team{Id: "tid"}
channel := &model.Channel{Id: "chid", TeamId: team.Id, Type: model.ChannelTypeOpen}
post := &model.Post{Id: "pid", ChannelId: channel.Id, Message: "tagging @" + user1.Username, UserId: user2.Id}
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
mockPostPersistentNotification.On("GetSingle", mock.Anything).Return(&model.PostPersistentNotifications{PostId: post.Id}, nil)
mockPostPersistentNotification.On("Delete", mock.Anything).Return(nil)
mockChannel := storemocks.ChannelStore{}
mockStore.On("Channel").Return(&mockChannel)
mockChannel.On("GetChannelsByIds", mock.Anything, mock.Anything).Return([]*model.Channel{channel}, nil)
mockChannel.On("GetAllChannelMembersNotifyPropsForChannel", mock.Anything, mock.Anything).Return(map[string]model.StringMap{}, nil)
mockTeam := storemocks.TeamStore{}
mockStore.On("Team").Return(&mockTeam)
mockTeam.On("GetMany", mock.Anything).Return([]*model.Team{team}, nil)
mockUser := storemocks.UserStore{}
mockStore.On("User").Return(&mockUser)
mockUser.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(profileMap, nil)
mockGroup := storemocks.GroupStore{}
mockStore.On("Group").Return(&mockGroup)
mockGroup.On("GetGroups", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Group{}, nil)
th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
cfg := th.App.Config()
*cfg.ServiceSettings.PostPriority = true
*cfg.ServiceSettings.AllowPersistentNotificationsForGuests = true
err := th.App.ResolvePersistentNotification(th.Context, post, user1.Id)
require.Nil(t, err)
mockPostPersistentNotification.AssertCalled(t, "Delete", mock.Anything)
})
t.Run("should not delete for post owner", func(t *testing.T) {
th := SetupWithStoreMock(t)
defer th.TearDown()
user1 := &model.User{Id: "uid1"}
post := &model.Post{Id: "test id", UserId: user1.Id}
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
err := th.App.ResolvePersistentNotification(th.Context, post, user1.Id)
require.Nil(t, err)
mockPostPersistentNotification.AssertNotCalled(t, "Delete", mock.Anything)
})
t.Run("should not delete for non-mentioned user", func(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY")
th := SetupWithStoreMock(t)
defer th.TearDown()
user1 := &model.User{Id: "uid1", Username: "user-1"}
user2 := &model.User{Id: "uid2", Username: "user-2"}
user3 := &model.User{Id: "uid3", Username: "user-3"}
profileMap := map[string]*model.User{user1.Id: user1, user2.Id: user2, user3.Id: user3}
team := &model.Team{Id: "tid"}
channel := &model.Channel{Id: "chid", TeamId: team.Id, Type: model.ChannelTypeOpen}
post := &model.Post{Id: "pid", ChannelId: channel.Id, Message: "tagging @" + user1.Username, UserId: user2.Id}
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
mockPostPersistentNotification.On("GetSingle", mock.Anything).Return(&model.PostPersistentNotifications{PostId: post.Id}, nil)
mockPostPersistentNotification.On("Delete", mock.Anything).Return(nil)
mockChannel := storemocks.ChannelStore{}
mockStore.On("Channel").Return(&mockChannel)
mockChannel.On("GetChannelsByIds", mock.Anything, mock.Anything).Return([]*model.Channel{channel}, nil)
mockChannel.On("GetAllChannelMembersNotifyPropsForChannel", mock.Anything, mock.Anything).Return(map[string]model.StringMap{}, nil)
mockTeam := storemocks.TeamStore{}
mockStore.On("Team").Return(&mockTeam)
mockTeam.On("GetMany", mock.Anything).Return([]*model.Team{team}, nil)
mockUser := storemocks.UserStore{}
mockStore.On("User").Return(&mockUser)
mockUser.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(profileMap, nil)
mockGroup := storemocks.GroupStore{}
mockStore.On("Group").Return(&mockGroup)
mockGroup.On("GetGroups", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Group{}, nil)
th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
cfg := th.App.Config()
*cfg.ServiceSettings.PostPriority = true
*cfg.ServiceSettings.AllowPersistentNotificationsForGuests = true
err := th.App.ResolvePersistentNotification(th.Context, post, user3.Id)
require.Nil(t, err)
mockPostPersistentNotification.AssertNotCalled(t, "Delete", mock.Anything)
})
}
func TestDeletePersistentNotification(t *testing.T) {
t.Run("should not delete when no posts exist", func(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY")
th := SetupWithStoreMock(t)
defer th.TearDown()
post := &model.Post{Id: "test id"}
mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
mockPostPersistentNotification.On("GetSingle", mock.Anything).Return(nil, &store.ErrNotFound{})
mockPostPersistentNotification.On("Delete", mock.Anything).Return(nil)
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
cfg := th.App.Config()
*cfg.ServiceSettings.PostPriority = true
*cfg.ServiceSettings.AllowPersistentNotificationsForGuests = true
err := th.App.DeletePersistentNotification(th.Context, post)
require.Nil(t, err)
mockPostPersistentNotification.AssertNotCalled(t, "Delete", mock.Anything)
})
t.Run("should delete", func(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY")
th := SetupWithStoreMock(t)
defer th.TearDown()
post := &model.Post{Id: "test id"}
mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
mockPostPersistentNotification.On("GetSingle", mock.Anything).Return(&model.PostPersistentNotifications{PostId: post.Id}, nil)
mockPostPersistentNotification.On("Delete", mock.Anything).Return(nil)
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
cfg := th.App.Config()
*cfg.ServiceSettings.PostPriority = true
err := th.App.DeletePersistentNotification(th.Context, post)
require.Nil(t, err)
mockPostPersistentNotification.AssertCalled(t, "Delete", mock.Anything)
})
}
func TestSendPersistentNotifications(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicChannel, false)
s := "Urgent"
tr := true
p1 := &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "test " + "@" + th.BasicUser2.Username,
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: &s,
PersistentNotifications: &tr,
},
},
}
_, appErr := th.App.CreatePost(th.Context, p1, th.BasicChannel, false, false)
require.Nil(t, appErr)
err := th.App.SendPersistentNotifications()
require.NoError(t, err)
}

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

@@ -32,3 +32,7 @@ func (a *App) GetPriorityForPostList(list *model.PostList) (map[string]*model.Po
return priorityMap, nil
}
func (a *App) IsPostPriorityEnabled() bool {
return a.Config().FeatureFlags.PostPriority && *a.Config().ServiceSettings.PostPriority
}

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

@@ -26,7 +26,7 @@ func (a *App) SaveReactionForPost(c *request.Context, reaction *model.Reaction)
}
if channel.DeleteAt > 0 {
return nil, model.NewAppError("deleteReactionForPost", "api.reaction.save.archived_channel.app_error", nil, "", http.StatusForbidden)
return nil, model.NewAppError("SaveReactionForPost", "api.reaction.save.archived_channel.app_error", nil, "", http.StatusForbidden)
}
reaction, nErr := a.Srv().Store().Reaction().Save(reaction)
@@ -40,6 +40,12 @@ func (a *App) SaveReactionForPost(c *request.Context, reaction *model.Reaction)
}
}
if post.RootId == "" {
if appErr := a.ResolvePersistentNotification(c, post, reaction.UserId); appErr != nil {
return nil, appErr
}
}
// The post is always modified since the UpdateAt always changes
a.invalidateCacheForChannelPosts(post.ChannelId)

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

@@ -51,6 +51,7 @@ import (
"github.com/mattermost/mattermost-server/server/v8/channels/jobs/migrations"
"github.com/mattermost/mattermost-server/server/v8/channels/jobs/notify_admin"
"github.com/mattermost/mattermost-server/server/v8/channels/jobs/plugins"
"github.com/mattermost/mattermost-server/server/v8/channels/jobs/post_persistent_notifications"
"github.com/mattermost/mattermost-server/server/v8/channels/jobs/product_notices"
"github.com/mattermost/mattermost-server/server/v8/channels/jobs/resend_invitation_email"
"github.com/mattermost/mattermost-server/server/v8/channels/product"
@@ -1582,6 +1583,12 @@ func (s *Server) initJobs() {
notify_admin.MakeScheduler(s.Jobs, s.License(), model.JobTypeTrialNotifyAdmin),
)
s.Jobs.RegisterJobType(
model.JobTypePostPersistentNotifications,
post_persistent_notifications.MakeWorker(s.Jobs, New(ServerConnector(s.Channels()))),
post_persistent_notifications.MakeScheduler(s.Jobs, func() *model.License { return s.License() }),
)
s.Jobs.RegisterJobType(
model.JobTypeInstallPluginNotifyAdmin,
notify_admin.MakeInstallPluginNotifyWorker(s.Jobs, New(ServerConnector(s.Channels()))),

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

@@ -1897,7 +1897,7 @@ func (a *App) GetTeamsUnreadForUser(excludeTeamId string, userID string, include
includeCollapsedThreads = includeCollapsedThreads && *a.Config().ServiceSettings.CollapsedThreads != model.CollapsedThreadsDisabled
if includeCollapsedThreads {
teamUnreads, err := a.Srv().Store().Thread().GetTeamsUnreadForUser(userID, teamIDs, a.isPostPriorityEnabled())
teamUnreads, err := a.Srv().Store().Thread().GetTeamsUnreadForUser(userID, teamIDs, a.IsPostPriorityEnabled())
if err != nil {
return nil, model.NewAppError("GetTeamsUnreadForUser", "app.team.get_unread.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
@@ -1976,6 +1976,10 @@ func (a *App) SoftDeleteTeam(teamID string) *model.AppError {
return err
}
if err := a.Srv().Store().PostPersistentNotification().DeleteByTeam([]string{team.Id}); err != nil {
return model.NewAppError("SoftDeleteTeam", "app.post_persistent_notification.delete_by_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
team.DeleteAt = model.GetMillis()
team, nErr := a.Srv().Store().Team().Update(team)
if nErr != nil {

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

@@ -2474,7 +2474,7 @@ func (a *App) ConvertBotToUser(c request.CTX, bot *model.Bot, userPatch *model.U
func (a *App) GetThreadsForUser(userID, teamID string, options model.GetUserThreadsOpts) (*model.Threads, *model.AppError) {
var result model.Threads
var eg errgroup.Group
postPriorityIsEnabled := a.isPostPriorityEnabled()
postPriorityIsEnabled := a.IsPostPriorityEnabled()
if postPriorityIsEnabled {
options.IncludeIsUrgent = true
}
@@ -2571,7 +2571,7 @@ func (a *App) GetThreadMembershipForUser(userId, threadId string) (*model.Thread
}
func (a *App) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool) (*model.ThreadResponse, *model.AppError) {
thread, nErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, extended, a.isPostPriorityEnabled())
thread, nErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, extended, a.IsPostPriorityEnabled())
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
@@ -2657,7 +2657,7 @@ func (a *App) UpdateThreadFollowForUserFromChannelAdd(c request.CTX, userID, tea
}
message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, teamID, "", userID, nil, "")
userThread, err := a.Srv().Store().Thread().GetThreadForUser(tm, true, a.isPostPriorityEnabled())
userThread, err := a.Srv().Store().Thread().GetThreadForUser(tm, true, a.IsPostPriorityEnabled())
if err != nil {
var errNotFound *store.ErrNotFound

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

@@ -216,6 +216,8 @@ channels/db/migrations/mysql/000107_threadmemberships_cleanup.down.sql
channels/db/migrations/mysql/000107_threadmemberships_cleanup.up.sql
channels/db/migrations/mysql/000108_remove_orphaned_oauth_preferences.down.sql
channels/db/migrations/mysql/000108_remove_orphaned_oauth_preferences.up.sql
channels/db/migrations/mysql/000109_create_persistent_notifications.down.sql
channels/db/migrations/mysql/000109_create_persistent_notifications.up.sql
channels/db/migrations/postgres/000001_create_teams.down.sql
channels/db/migrations/postgres/000001_create_teams.up.sql
channels/db/migrations/postgres/000002_create_team_members.down.sql
@@ -432,3 +434,5 @@ channels/db/migrations/postgres/000107_threadmemberships_cleanup.down.sql
channels/db/migrations/postgres/000107_threadmemberships_cleanup.up.sql
channels/db/migrations/postgres/000108_remove_orphaned_oauth_preferences.down.sql
channels/db/migrations/postgres/000108_remove_orphaned_oauth_preferences.up.sql
channels/db/migrations/postgres/000109_create_persistent_notifications.down.sql
channels/db/migrations/postgres/000109_create_persistent_notifications.up.sql

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

@@ -0,0 +1 @@
DROP TABLE IF EXISTS PersistentNotifications;

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

@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS PersistentNotifications (
PostId varchar(26) NOT NULL,
CreateAt bigint(20) DEFAULT NULL,
LastSentAt bigint(20) DEFAULT NULL,
DeleteAt bigint(20) DEFAULT NULL,
SentCount smallint DEFAULT NULL,
PRIMARY KEY (PostId)
);

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

@@ -0,0 +1 @@
DROP TABLE IF EXISTS persistentnotifications;

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

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS persistentnotifications (
postid VARCHAR(26) PRIMARY KEY,
createat bigint,
lastsentat bigint,
deleteat bigint,
sentcount smallint
);

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

@@ -0,0 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package post_persistent_notifications
import (
"time"
"github.com/mattermost/mattermost-server/server/public/model"
"github.com/mattermost/mattermost-server/server/v8/channels/jobs"
)
type Scheduler struct {
*jobs.PeriodicScheduler
}
func (scheduler *Scheduler) NextScheduleTime(cfg *model.Config, _ time.Time, _ bool, _ *model.Job) *time.Time {
nextTime := time.Now().Add((time.Duration(*cfg.ServiceSettings.PersistentNotificationIntervalMinutes) * time.Minute) / 2)
return &nextTime
}
func MakeScheduler(jobServer *jobs.JobServer, licenseFunc func() *model.License) model.Scheduler {
enabledFunc := func(_ *model.Config) bool {
l := licenseFunc()
return l != nil && (l.SkuShortName == model.LicenseShortSkuProfessional || l.SkuShortName == model.LicenseShortSkuEnterprise)
}
return &Scheduler{jobs.NewPeriodicScheduler(jobServer, model.JobTypePostPersistentNotifications, 0, enabledFunc)}
}

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

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package post_persistent_notifications
import (
"github.com/mattermost/mattermost-server/server/public/model"
"github.com/mattermost/mattermost-server/server/v8/channels/jobs"
)
const (
JobName = "PostPersistentNotifications"
)
type AppIface interface {
SendPersistentNotifications() error
IsPersistentNotificationsEnabled() bool
}
func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker {
isEnabled := func(_ *model.Config) bool {
return app.IsPersistentNotificationsEnabled()
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
return app.SendPersistentNotifications()
}
worker := jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled)
return worker
}

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

@@ -19,48 +19,49 @@ import (
type OpenTracingLayer struct {
store.Store
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
ChannelMemberHistoryStore store.ChannelMemberHistoryStore
ClusterDiscoveryStore store.ClusterDiscoveryStore
CommandStore store.CommandStore
CommandWebhookStore store.CommandWebhookStore
ComplianceStore store.ComplianceStore
DraftStore store.DraftStore
EmojiStore store.EmojiStore
FileInfoStore store.FileInfoStore
GroupStore store.GroupStore
JobStore store.JobStore
LicenseStore store.LicenseStore
LinkMetadataStore store.LinkMetadataStore
NotifyAdminStore store.NotifyAdminStore
OAuthStore store.OAuthStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
ReactionStore store.ReactionStore
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
RoleStore store.RoleStore
SchemeStore store.SchemeStore
SessionStore store.SessionStore
SharedChannelStore store.SharedChannelStore
StatusStore store.StatusStore
SystemStore store.SystemStore
TeamStore store.TeamStore
TermsOfServiceStore store.TermsOfServiceStore
ThreadStore store.ThreadStore
TokenStore store.TokenStore
TrueUpReviewStore store.TrueUpReviewStore
UploadSessionStore store.UploadSessionStore
UserStore store.UserStore
UserAccessTokenStore store.UserAccessTokenStore
UserTermsOfServiceStore store.UserTermsOfServiceStore
WebhookStore store.WebhookStore
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
ChannelMemberHistoryStore store.ChannelMemberHistoryStore
ClusterDiscoveryStore store.ClusterDiscoveryStore
CommandStore store.CommandStore
CommandWebhookStore store.CommandWebhookStore
ComplianceStore store.ComplianceStore
DraftStore store.DraftStore
EmojiStore store.EmojiStore
FileInfoStore store.FileInfoStore
GroupStore store.GroupStore
JobStore store.JobStore
LicenseStore store.LicenseStore
LinkMetadataStore store.LinkMetadataStore
NotifyAdminStore store.NotifyAdminStore
OAuthStore store.OAuthStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
PostPersistentNotificationStore store.PostPersistentNotificationStore
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
ReactionStore store.ReactionStore
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
RoleStore store.RoleStore
SchemeStore store.SchemeStore
SessionStore store.SessionStore
SharedChannelStore store.SharedChannelStore
StatusStore store.StatusStore
SystemStore store.SystemStore
TeamStore store.TeamStore
TermsOfServiceStore store.TermsOfServiceStore
ThreadStore store.ThreadStore
TokenStore store.TokenStore
TrueUpReviewStore store.TrueUpReviewStore
UploadSessionStore store.UploadSessionStore
UserStore store.UserStore
UserAccessTokenStore store.UserAccessTokenStore
UserTermsOfServiceStore store.UserTermsOfServiceStore
WebhookStore store.WebhookStore
}
func (s *OpenTracingLayer) Audit() store.AuditStore {
@@ -143,6 +144,10 @@ func (s *OpenTracingLayer) PostAcknowledgement() store.PostAcknowledgementStore
return s.PostAcknowledgementStore
}
func (s *OpenTracingLayer) PostPersistentNotification() store.PostPersistentNotificationStore {
return s.PostPersistentNotificationStore
}
func (s *OpenTracingLayer) PostPriority() store.PostPriorityStore {
return s.PostPriorityStore
}
@@ -331,6 +336,11 @@ type OpenTracingLayerPostAcknowledgementStore struct {
Root *OpenTracingLayer
}
type OpenTracingLayerPostPersistentNotificationStore struct {
store.PostPersistentNotificationStore
Root *OpenTracingLayer
}
type OpenTracingLayerPostPriorityStore struct {
store.PostPriorityStore
Root *OpenTracingLayer
@@ -6839,6 +6849,132 @@ func (s *OpenTracingLayerPostAcknowledgementStore) Save(postID string, userID st
return result, err
}
func (s *OpenTracingLayerPostPersistentNotificationStore) Delete(postIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostPersistentNotificationStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostPersistentNotificationStore.Delete(postIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostPersistentNotificationStore) DeleteByChannel(channelIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostPersistentNotificationStore.DeleteByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostPersistentNotificationStore.DeleteByChannel(channelIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostPersistentNotificationStore) DeleteByTeam(teamIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostPersistentNotificationStore.DeleteByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostPersistentNotificationStore.DeleteByTeam(teamIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostPersistentNotificationStore) DeleteExpired(maxSentCount int16) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostPersistentNotificationStore.DeleteExpired")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostPersistentNotificationStore.DeleteExpired(maxSentCount)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostPersistentNotificationStore) Get(params model.GetPersistentNotificationsPostsParams) ([]*model.PostPersistentNotifications, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostPersistentNotificationStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostPersistentNotificationStore.Get(params)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostPersistentNotificationStore) GetSingle(postID string) (*model.PostPersistentNotifications, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostPersistentNotificationStore.GetSingle")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostPersistentNotificationStore.GetSingle(postID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostPersistentNotificationStore) UpdateLastActivity(postIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostPersistentNotificationStore.UpdateLastActivity")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostPersistentNotificationStore.UpdateLastActivity(postIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostPriorityStore.GetForPost")
@@ -12942,6 +13078,7 @@ func New(childStore store.Store, ctx context.Context) *OpenTracingLayer {
newStore.PluginStore = &OpenTracingLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
newStore.PostStore = &OpenTracingLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
newStore.PostAcknowledgementStore = &OpenTracingLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore}
newStore.PostPersistentNotificationStore = &OpenTracingLayerPostPersistentNotificationStore{PostPersistentNotificationStore: childStore.PostPersistentNotification(), Root: &newStore}
newStore.PostPriorityStore = &OpenTracingLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore}
newStore.PreferenceStore = &OpenTracingLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
newStore.ProductNoticesStore = &OpenTracingLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}

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

@@ -22,48 +22,49 @@ const mySQLDeadlockCode = uint16(1213)
type RetryLayer struct {
store.Store
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
ChannelMemberHistoryStore store.ChannelMemberHistoryStore
ClusterDiscoveryStore store.ClusterDiscoveryStore
CommandStore store.CommandStore
CommandWebhookStore store.CommandWebhookStore
ComplianceStore store.ComplianceStore
DraftStore store.DraftStore
EmojiStore store.EmojiStore
FileInfoStore store.FileInfoStore
GroupStore store.GroupStore
JobStore store.JobStore
LicenseStore store.LicenseStore
LinkMetadataStore store.LinkMetadataStore
NotifyAdminStore store.NotifyAdminStore
OAuthStore store.OAuthStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
ReactionStore store.ReactionStore
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
RoleStore store.RoleStore
SchemeStore store.SchemeStore
SessionStore store.SessionStore
SharedChannelStore store.SharedChannelStore
StatusStore store.StatusStore
SystemStore store.SystemStore
TeamStore store.TeamStore
TermsOfServiceStore store.TermsOfServiceStore
ThreadStore store.ThreadStore
TokenStore store.TokenStore
TrueUpReviewStore store.TrueUpReviewStore
UploadSessionStore store.UploadSessionStore
UserStore store.UserStore
UserAccessTokenStore store.UserAccessTokenStore
UserTermsOfServiceStore store.UserTermsOfServiceStore
WebhookStore store.WebhookStore
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
ChannelMemberHistoryStore store.ChannelMemberHistoryStore
ClusterDiscoveryStore store.ClusterDiscoveryStore
CommandStore store.CommandStore
CommandWebhookStore store.CommandWebhookStore
ComplianceStore store.ComplianceStore
DraftStore store.DraftStore
EmojiStore store.EmojiStore
FileInfoStore store.FileInfoStore
GroupStore store.GroupStore
JobStore store.JobStore
LicenseStore store.LicenseStore
LinkMetadataStore store.LinkMetadataStore
NotifyAdminStore store.NotifyAdminStore
OAuthStore store.OAuthStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
PostPersistentNotificationStore store.PostPersistentNotificationStore
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
ReactionStore store.ReactionStore
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
RoleStore store.RoleStore
SchemeStore store.SchemeStore
SessionStore store.SessionStore
SharedChannelStore store.SharedChannelStore
StatusStore store.StatusStore
SystemStore store.SystemStore
TeamStore store.TeamStore
TermsOfServiceStore store.TermsOfServiceStore
ThreadStore store.ThreadStore
TokenStore store.TokenStore
TrueUpReviewStore store.TrueUpReviewStore
UploadSessionStore store.UploadSessionStore
UserStore store.UserStore
UserAccessTokenStore store.UserAccessTokenStore
UserTermsOfServiceStore store.UserTermsOfServiceStore
WebhookStore store.WebhookStore
}
func (s *RetryLayer) Audit() store.AuditStore {
@@ -146,6 +147,10 @@ func (s *RetryLayer) PostAcknowledgement() store.PostAcknowledgementStore {
return s.PostAcknowledgementStore
}
func (s *RetryLayer) PostPersistentNotification() store.PostPersistentNotificationStore {
return s.PostPersistentNotificationStore
}
func (s *RetryLayer) PostPriority() store.PostPriorityStore {
return s.PostPriorityStore
}
@@ -334,6 +339,11 @@ type RetryLayerPostAcknowledgementStore struct {
Root *RetryLayer
}
type RetryLayerPostPersistentNotificationStore struct {
store.PostPersistentNotificationStore
Root *RetryLayer
}
type RetryLayerPostPriorityStore struct {
store.PostPriorityStore
Root *RetryLayer
@@ -7750,6 +7760,153 @@ func (s *RetryLayerPostAcknowledgementStore) Save(postID string, userID string,
}
func (s *RetryLayerPostPersistentNotificationStore) Delete(postIds []string) error {
tries := 0
for {
err := s.PostPersistentNotificationStore.Delete(postIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostPersistentNotificationStore) DeleteByChannel(channelIds []string) error {
tries := 0
for {
err := s.PostPersistentNotificationStore.DeleteByChannel(channelIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostPersistentNotificationStore) DeleteByTeam(teamIds []string) error {
tries := 0
for {
err := s.PostPersistentNotificationStore.DeleteByTeam(teamIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostPersistentNotificationStore) DeleteExpired(maxSentCount int16) error {
tries := 0
for {
err := s.PostPersistentNotificationStore.DeleteExpired(maxSentCount)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostPersistentNotificationStore) Get(params model.GetPersistentNotificationsPostsParams) ([]*model.PostPersistentNotifications, error) {
tries := 0
for {
result, err := s.PostPersistentNotificationStore.Get(params)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostPersistentNotificationStore) GetSingle(postID string) (*model.PostPersistentNotifications, error) {
tries := 0
for {
result, err := s.PostPersistentNotificationStore.GetSingle(postID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostPersistentNotificationStore) UpdateLastActivity(postIds []string) error {
tries := 0
for {
err := s.PostPersistentNotificationStore.UpdateLastActivity(postIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
tries := 0
@@ -14750,6 +14907,7 @@ func New(childStore store.Store) *RetryLayer {
newStore.PluginStore = &RetryLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
newStore.PostStore = &RetryLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
newStore.PostAcknowledgementStore = &RetryLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore}
newStore.PostPersistentNotificationStore = &RetryLayerPostPersistentNotificationStore{PostPersistentNotificationStore: childStore.PostPersistentNotification(), Root: &newStore}
newStore.PostPriorityStore = &RetryLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore}
newStore.PreferenceStore = &RetryLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
newStore.ProductNoticesStore = &RetryLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}

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

@@ -57,6 +57,7 @@ func genStore() *mocks.Store {
mock.On("Draft").Return(&mocks.DraftStore{})
mock.On("PostPriority").Return(&mocks.PostPriorityStore{})
mock.On("PostAcknowledgement").Return(&mocks.PostAcknowledgementStore{})
mock.On("PostPersistentNotification").Return(&mocks.PostPersistentNotificationStore{})
mock.On("TrueUpReview").Return(&mocks.TrueUpReviewStore{})
return mock
}

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

@@ -0,0 +1,193 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"github.com/mattermost/mattermost-server/server/public/model"
"github.com/mattermost/mattermost-server/server/v8/channels/store"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
)
type SqlPostPersistentNotificationStore struct {
*SqlStore
}
func newSqlPostPersistentNotificationStore(sqlStore *SqlStore) store.PostPersistentNotificationStore {
return &SqlPostPersistentNotificationStore{
SqlStore: sqlStore,
}
}
func (s *SqlPostPersistentNotificationStore) GetSingle(postID string) (*model.PostPersistentNotifications, error) {
builder := s.getQueryBuilder().
Select("PostId, CreateAt, LastSentAt, DeleteAt, SentCount").
From("PersistentNotifications").
Where(sq.And{
sq.Eq{"DeleteAt": 0},
sq.Eq{"PostId": postID},
})
post := &model.PostPersistentNotifications{}
err := s.GetReplicaX().GetBuilder(post, builder)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Persistent Notification Post", postID)
}
return nil, errors.Wrapf(err, "failed to get the persistent notification post=%s", postID)
}
return post, nil
}
// Get returns only valid posts.
func (s *SqlPostPersistentNotificationStore) Get(params model.GetPersistentNotificationsPostsParams) ([]*model.PostPersistentNotifications, error) {
if params.PerPage == 0 {
params.PerPage = 1000
}
builder := s.getQueryBuilder().
Select("PostId, CreateAt, LastSentAt, DeleteAt, SentCount").
From("PersistentNotifications").
Where(sq.And{
sq.Eq{"DeleteAt": 0},
sq.LtOrEq{"CreateAt": params.MaxTime},
sq.LtOrEq{"LastSentAt": params.MaxTime},
sq.Lt{"SentCount": params.MaxSentCount},
}).
Limit(uint64(params.PerPage))
var posts []*model.PostPersistentNotifications
// Replica may not have the latest changes(done by UpdateLastActivity func)
// by the time this Get func is called again in the loop.
err := s.GetMasterX().SelectBuilder(&posts, builder)
if err != nil {
return nil, errors.Wrap(err, "failed to get notifications")
}
return posts, nil
}
func (s *SqlPostPersistentNotificationStore) UpdateLastActivity(postIds []string) error {
builder := s.getQueryBuilder().
Update("PersistentNotifications").
Set("LastSentAt", model.GetMillis()).
Set("SentCount", sq.Expr("SentCount+1")).
Where(sq.Eq{"PostId": postIds})
_, err := s.GetMasterX().ExecBuilder(builder)
if err != nil {
return errors.Wrapf(err, "failed to update last activity for posts %s", postIds)
}
return nil
}
func (s *SqlPostPersistentNotificationStore) Delete(postIds []string) error {
count := len(postIds)
if count == 0 {
return nil
}
builder := s.getQueryBuilder().
Update("PersistentNotifications").
Set("DeleteAt", model.GetMillis()).
Where(sq.Eq{"PostId": postIds})
_, err := s.GetMasterX().ExecBuilder(builder)
if err != nil {
return errors.Wrapf(err, "failed to delete notifications for posts %s", postIds)
}
return nil
}
func (s *SqlPostPersistentNotificationStore) DeleteExpired(maxSentCount int16) error {
builder := s.getQueryBuilder().
Update("PersistentNotifications").
Set("DeleteAt", model.GetMillis()).
Where(sq.And{
sq.Eq{"DeleteAt": 0},
sq.GtOrEq{"SentCount": maxSentCount},
})
_, err := s.GetMasterX().ExecBuilder(builder)
if err != nil {
return errors.Wrap(err, "failed to delete notifications")
}
return nil
}
func (s *SqlPostPersistentNotificationStore) DeleteByChannel(channelIds []string) error {
count := len(channelIds)
if count == 0 {
return nil
}
deleteAt := model.GetMillis()
var builder sq.UpdateBuilder
builderType := s.getQueryBuilder()
if s.DriverName() == model.DatabaseDriverMysql {
builder = builderType.
Update("PersistentNotifications, Posts").
Set("PersistentNotifications.DeleteAt", deleteAt)
}
if s.DriverName() == model.DatabaseDriverPostgres {
builder = builderType.
Update("PersistentNotifications").
Set("DeleteAt", deleteAt).
From("Posts")
}
builder = builder.Where(sq.And{
sq.Expr("Posts.Id = PersistentNotifications.PostId"),
sq.Eq{"Posts.ChannelId": channelIds},
})
_, err := s.GetMasterX().ExecBuilder(builder)
if err != nil {
return errors.Wrapf(err, "failed to delete notifications for channels %s", channelIds)
}
return nil
}
func (s *SqlPostPersistentNotificationStore) DeleteByTeam(teamIds []string) error {
count := len(teamIds)
if count == 0 {
return nil
}
deleteAt := model.GetMillis()
var builder sq.UpdateBuilder
builderType := s.getQueryBuilder()
if s.DriverName() == model.DatabaseDriverMysql {
builder = builderType.
Update("PersistentNotifications, Posts, Channels").
Set("PersistentNotifications.DeleteAt", deleteAt)
}
if s.DriverName() == model.DatabaseDriverPostgres {
builder = builderType.
Update("PersistentNotifications").
Set("DeleteAt", deleteAt).
From("Posts, Channels")
}
builder = builder.Where(sq.And{
sq.Expr("Posts.Id = PersistentNotifications.PostId"),
sq.Expr("Posts.ChannelId = Channels.Id"),
sq.Eq{"Channels.TeamId": teamIds},
})
_, err := s.GetMasterX().ExecBuilder(builder)
if err != nil {
return errors.Wrapf(err, "failed to delete notifications for teams %s", teamIds)
}
return nil
}

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

@@ -0,0 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"testing"
"github.com/mattermost/mattermost-server/server/v8/channels/store/storetest"
)
func TestPostPersistentNotificationStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestPostPersistentNotificationStore)
}

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

@@ -223,6 +223,10 @@ func (s *SqlPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, er
return nil, -1, errors.Wrap(err, "failed to save PostPriority")
}
if err = s.savePostsPersistentNotifications(transaction, posts); err != nil {
return nil, -1, errors.Wrap(err, "failed to save posts persistent notifications")
}
if err = transaction.Commit(); err != nil {
// don't need to rollback here since the transaction is already closed
return posts, -1, errors.Wrap(err, "commit_transaction")
@@ -3004,6 +3008,20 @@ func (s *SqlPostStore) savePostsPriority(transaction *sqlxTxWrapper, posts []*mo
return nil
}
func (s *SqlPostStore) savePostsPersistentNotifications(transaction *sqlxTxWrapper, posts []*model.Post) error {
for _, post := range posts {
if priority := post.GetPriority(); priority != nil && priority.PersistentNotifications != nil && *priority.PersistentNotifications {
if _, err := transaction.NamedExec(`INSERT INTO PersistentNotifications (PostId, CreateAt, LastSentAt, DeleteAt, SentCount) VALUES (:PostId, :CreateAt, :LastSentAt, :DeleteAt, :SentCount)`, &model.PostPersistentNotifications{
PostId: post.Id,
CreateAt: post.CreateAt,
}); err != nil {
return err
}
}
}
return nil
}
func (s *SqlPostStore) updateThreadsFromPosts(transaction *sqlxTxWrapper, posts []*model.Post) error {
postsByRoot := map[string][]*model.Post{}
var rootIds []string

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

@@ -71,48 +71,49 @@ const (
var tablesToCheckForCollation = []string{"incomingwebhooks", "preferences", "users", "uploadsessions", "channels", "publicchannels"}
type SqlStoreStores struct {
team store.TeamStore
channel store.ChannelStore
post store.PostStore
retentionPolicy store.RetentionPolicyStore
thread store.ThreadStore
user store.UserStore
bot store.BotStore
audit store.AuditStore
cluster store.ClusterDiscoveryStore
remoteCluster store.RemoteClusterStore
compliance store.ComplianceStore
session store.SessionStore
oauth store.OAuthStore
system store.SystemStore
webhook store.WebhookStore
command store.CommandStore
commandWebhook store.CommandWebhookStore
preference store.PreferenceStore
license store.LicenseStore
token store.TokenStore
emoji store.EmojiStore
status store.StatusStore
fileInfo store.FileInfoStore
uploadSession store.UploadSessionStore
reaction store.ReactionStore
job store.JobStore
userAccessToken store.UserAccessTokenStore
plugin store.PluginStore
channelMemberHistory store.ChannelMemberHistoryStore
role store.RoleStore
scheme store.SchemeStore
TermsOfService store.TermsOfServiceStore
productNotices store.ProductNoticesStore
group store.GroupStore
UserTermsOfService store.UserTermsOfServiceStore
linkMetadata store.LinkMetadataStore
sharedchannel store.SharedChannelStore
draft store.DraftStore
notifyAdmin store.NotifyAdminStore
postPriority store.PostPriorityStore
postAcknowledgement store.PostAcknowledgementStore
trueUpReview store.TrueUpReviewStore
team store.TeamStore
channel store.ChannelStore
post store.PostStore
retentionPolicy store.RetentionPolicyStore
thread store.ThreadStore
user store.UserStore
bot store.BotStore
audit store.AuditStore
cluster store.ClusterDiscoveryStore
remoteCluster store.RemoteClusterStore
compliance store.ComplianceStore
session store.SessionStore
oauth store.OAuthStore
system store.SystemStore
webhook store.WebhookStore
command store.CommandStore
commandWebhook store.CommandWebhookStore
preference store.PreferenceStore
license store.LicenseStore
token store.TokenStore
emoji store.EmojiStore
status store.StatusStore
fileInfo store.FileInfoStore
uploadSession store.UploadSessionStore
reaction store.ReactionStore
job store.JobStore
userAccessToken store.UserAccessTokenStore
plugin store.PluginStore
channelMemberHistory store.ChannelMemberHistoryStore
role store.RoleStore
scheme store.SchemeStore
TermsOfService store.TermsOfServiceStore
productNotices store.ProductNoticesStore
group store.GroupStore
UserTermsOfService store.UserTermsOfServiceStore
linkMetadata store.LinkMetadataStore
sharedchannel store.SharedChannelStore
draft store.DraftStore
notifyAdmin store.NotifyAdminStore
postPriority store.PostPriorityStore
postAcknowledgement store.PostAcknowledgementStore
postPersistentNotification store.PostPersistentNotificationStore
trueUpReview store.TrueUpReviewStore
}
type SqlStore struct {
@@ -232,6 +233,7 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlS
store.stores.notifyAdmin = newSqlNotifyAdminStore(store)
store.stores.postPriority = newSqlPostPriorityStore(store)
store.stores.postAcknowledgement = newSqlPostAcknowledgementStore(store)
store.stores.postPersistentNotification = newSqlPostPersistentNotificationStore(store)
store.stores.trueUpReview = newSqlTrueUpReviewStore(store)
store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures()
@@ -1076,6 +1078,10 @@ func (ss *SqlStore) PostAcknowledgement() store.PostAcknowledgementStore {
return ss.stores.postAcknowledgement
}
func (ss *SqlStore) PostPersistentNotification() store.PostPersistentNotificationStore {
return ss.stores.postPersistentNotification
}
func (ss *SqlStore) TrueUpReview() store.TrueUpReviewStore {
return ss.stores.trueUpReview
}

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

@@ -84,6 +84,7 @@ type Store interface {
NotifyAdmin() NotifyAdminStore
PostPriority() PostPriorityStore
PostAcknowledgement() PostAcknowledgementStore
PostPersistentNotification() PostPersistentNotificationStore
TrueUpReview() TrueUpReviewStore
}
@@ -999,6 +1000,16 @@ type PostAcknowledgementStore interface {
Delete(acknowledgement *model.PostAcknowledgement) error
}
type PostPersistentNotificationStore interface {
Get(params model.GetPersistentNotificationsPostsParams) ([]*model.PostPersistentNotifications, error)
GetSingle(postID string) (*model.PostPersistentNotifications, error)
UpdateLastActivity(postIds []string) error
Delete(postIds []string) error
DeleteExpired(maxSentCount int16) error
DeleteByChannel(channelIds []string) error
DeleteByTeam(teamIds []string) error
}
type TrueUpReviewStore interface {
GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error)
CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error)

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

@@ -0,0 +1,152 @@
// Code generated by mockery v2.23.2. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/server/public/model"
mock "github.com/stretchr/testify/mock"
)
// PostPersistentNotificationStore is an autogenerated mock type for the PostPersistentNotificationStore type
type PostPersistentNotificationStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: postIds
func (_m *PostPersistentNotificationStore) Delete(postIds []string) error {
ret := _m.Called(postIds)
var r0 error
if rf, ok := ret.Get(0).(func([]string) error); ok {
r0 = rf(postIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteByChannel provides a mock function with given fields: channelIds
func (_m *PostPersistentNotificationStore) DeleteByChannel(channelIds []string) error {
ret := _m.Called(channelIds)
var r0 error
if rf, ok := ret.Get(0).(func([]string) error); ok {
r0 = rf(channelIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteByTeam provides a mock function with given fields: teamIds
func (_m *PostPersistentNotificationStore) DeleteByTeam(teamIds []string) error {
ret := _m.Called(teamIds)
var r0 error
if rf, ok := ret.Get(0).(func([]string) error); ok {
r0 = rf(teamIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteExpired provides a mock function with given fields: maxSentCount
func (_m *PostPersistentNotificationStore) DeleteExpired(maxSentCount int16) error {
ret := _m.Called(maxSentCount)
var r0 error
if rf, ok := ret.Get(0).(func(int16) error); ok {
r0 = rf(maxSentCount)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: params
func (_m *PostPersistentNotificationStore) Get(params model.GetPersistentNotificationsPostsParams) ([]*model.PostPersistentNotifications, error) {
ret := _m.Called(params)
var r0 []*model.PostPersistentNotifications
var r1 error
if rf, ok := ret.Get(0).(func(model.GetPersistentNotificationsPostsParams) ([]*model.PostPersistentNotifications, error)); ok {
return rf(params)
}
if rf, ok := ret.Get(0).(func(model.GetPersistentNotificationsPostsParams) []*model.PostPersistentNotifications); ok {
r0 = rf(params)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PostPersistentNotifications)
}
}
if rf, ok := ret.Get(1).(func(model.GetPersistentNotificationsPostsParams) error); ok {
r1 = rf(params)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSingle provides a mock function with given fields: postID
func (_m *PostPersistentNotificationStore) GetSingle(postID string) (*model.PostPersistentNotifications, error) {
ret := _m.Called(postID)
var r0 *model.PostPersistentNotifications
var r1 error
if rf, ok := ret.Get(0).(func(string) (*model.PostPersistentNotifications, error)); ok {
return rf(postID)
}
if rf, ok := ret.Get(0).(func(string) *model.PostPersistentNotifications); ok {
r0 = rf(postID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostPersistentNotifications)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(postID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateLastActivity provides a mock function with given fields: postIds
func (_m *PostPersistentNotificationStore) UpdateLastActivity(postIds []string) error {
ret := _m.Called(postIds)
var r0 error
if rf, ok := ret.Get(0).(func([]string) error); ok {
r0 = rf(postIds)
} else {
r0 = ret.Error(0)
}
return r0
}
type mockConstructorTestingTNewPostPersistentNotificationStore interface {
mock.TestingT
Cleanup(func())
}
// NewPostPersistentNotificationStore creates a new instance of PostPersistentNotificationStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewPostPersistentNotificationStore(t mockConstructorTestingTNewPostPersistentNotificationStore) *PostPersistentNotificationStore {
mock := &PostPersistentNotificationStore{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

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

@@ -500,6 +500,22 @@ func (_m *Store) PostAcknowledgement() store.PostAcknowledgementStore {
return r0
}
// PostPersistentNotification provides a mock function with given fields:
func (_m *Store) PostPersistentNotification() store.PostPersistentNotificationStore {
ret := _m.Called()
var r0 store.PostPersistentNotificationStore
if rf, ok := ret.Get(0).(func() store.PostPersistentNotificationStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.PostPersistentNotificationStore)
}
}
return r0
}
// PostPriority provides a mock function with given fields:
func (_m *Store) PostPriority() store.PostPriorityStore {
ret := _m.Called()

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

@@ -0,0 +1,452 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"time"
"github.com/mattermost/mattermost-server/server/public/model"
"github.com/mattermost/mattermost-server/server/v8/channels/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPostPersistentNotificationStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("Get", func(t *testing.T) { testPostPersistentNotificationStoreGet(t, ss) })
t.Run("Delete", func(t *testing.T) { testPostPersistentNotificationStoreDelete(t, ss) })
t.Run("UpdateLastSentAt", func(t *testing.T) { testPostPersistentNotificationStoreUpdateLastSentAt(t, ss) })
}
func testPostPersistentNotificationStoreGet(t *testing.T, ss store.Store) {
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.CreateAt = 10
p1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(true),
},
}
p2 := model.Post{}
p2.ChannelId = p1.ChannelId
p2.UserId = model.NewId()
p2.Message = NewTestId()
p2.CreateAt = 20
p2.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(true),
},
}
// Invalid - Has no Priority
p3 := model.Post{}
p3.ChannelId = p1.ChannelId
p3.UserId = model.NewId()
p3.Message = NewTestId()
p3.CreateAt = 30
// Invalid - Notification is false
p4 := model.Post{}
p4.ChannelId = p1.ChannelId
p4.UserId = model.NewId()
p4.Message = NewTestId()
p4.CreateAt = 40
p4.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(false),
},
}
p5 := model.Post{}
p5.ChannelId = p1.ChannelId
p5.UserId = model.NewId()
p5.Message = NewTestId()
p5.CreateAt = 50
p5.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(true),
},
}
_, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2, &p3, &p4, &p5})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
defer ss.Post().PermanentDeleteByChannel(p1.ChannelId)
defer ss.PostPersistentNotification().Delete([]string{p1.Id, p2.Id, p3.Id, p4.Id, p5.Id})
t.Run("Get Single", func(t *testing.T) {
pn, err := ss.PostPersistentNotification().GetSingle(p1.Id)
require.NoError(t, err)
assert.Equal(t, p1.Id, pn.PostId)
pn, err = ss.PostPersistentNotification().GetSingle(p2.Id)
require.NoError(t, err)
assert.Equal(t, p2.Id, pn.PostId)
pn, err = ss.PostPersistentNotification().GetSingle(p5.Id)
require.NoError(t, err)
assert.Equal(t, p5.Id, pn.PostId)
pn, err = ss.PostPersistentNotification().GetSingle(p3.Id)
require.Error(t, err)
require.Zero(t, pn)
pn, err = ss.PostPersistentNotification().GetSingle(p4.Id)
require.Error(t, err)
require.Zero(t, pn)
})
t.Run("Get all before MaxTime", func(t *testing.T) {
validIDs := []string{p1.Id, p2.Id, p5.Id}
getIDs := func(posts []*model.PostPersistentNotifications) (ids []string) {
for _, p := range posts {
ids = append(ids, p.PostId)
}
return
}
// p5 is filtered by maxTime
pn, err := ss.PostPersistentNotification().Get(model.GetPersistentNotificationsPostsParams{
MaxTime: 45,
MaxSentCount: 60,
PerPage: 20,
})
require.NoError(t, err)
require.Len(t, pn, 2)
assert.Contains(t, getIDs(pn), p1.Id)
assert.Contains(t, getIDs(pn), p2.Id)
// nothing is filtered out
pn, err = ss.PostPersistentNotification().Get(model.GetPersistentNotificationsPostsParams{
MaxTime: 100,
MaxSentCount: 60,
PerPage: 20,
})
require.NoError(t, err)
require.Len(t, pn, 3)
assert.ElementsMatch(t, validIDs, getIDs(pn))
})
}
func testPostPersistentNotificationStoreUpdateLastSentAt(t *testing.T, ss store.Store) {
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.CreateAt = 10
p1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(true),
},
}
_, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
defer ss.Post().PermanentDeleteByChannel(p1.ChannelId)
defer ss.PostPersistentNotification().Delete([]string{p1.Id})
// Update from 0 value
now := model.GetTimeForMillis(model.GetMillis())
delta := 2 * time.Second
err = ss.PostPersistentNotification().UpdateLastActivity([]string{p1.Id})
require.NoError(t, err)
pn, err := ss.PostPersistentNotification().Get(model.GetPersistentNotificationsPostsParams{
MaxTime: model.GetMillisForTime(now.Add(delta)),
MaxSentCount: 60,
})
require.NoError(t, err)
require.Len(t, pn, 1)
assert.WithinDuration(t, now, model.GetTimeForMillis(pn[0].LastSentAt), delta)
time.Sleep(time.Second)
// Update from non-zero value
now = model.GetTimeForMillis(model.GetMillis())
delta = 2 * time.Second
err = ss.PostPersistentNotification().UpdateLastActivity([]string{p1.Id})
require.NoError(t, err)
pn, err = ss.PostPersistentNotification().Get(model.GetPersistentNotificationsPostsParams{
MaxTime: model.GetMillisForTime(now.Add(delta)),
MaxSentCount: 60,
})
require.NoError(t, err)
require.Len(t, pn, 1)
assert.WithinDuration(t, now, model.GetTimeForMillis(pn[0].LastSentAt), delta)
}
func testPostPersistentNotificationStoreDelete(t *testing.T, ss store.Store) {
t.Run("Delete", func(t *testing.T) {
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.CreateAt = 10
p1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(true),
},
}
p2 := model.Post{}
p2.ChannelId = p1.ChannelId
p2.UserId = model.NewId()
p2.Message = NewTestId()
p2.CreateAt = 20
p2.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(true),
},
}
p3 := model.Post{}
p3.ChannelId = p1.ChannelId
p3.UserId = model.NewId()
p3.Message = NewTestId()
p3.CreateAt = 30
p3.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(true),
},
}
_, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2, &p3})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
defer ss.Post().PermanentDeleteByChannel(p1.ChannelId)
defer ss.PostPersistentNotification().Delete([]string{p1.Id, p2.Id, p3.Id})
err = ss.PostPersistentNotification().Delete([]string{p1.Id, p3.Id})
require.NoError(t, err)
pn, err := ss.PostPersistentNotification().Get(model.GetPersistentNotificationsPostsParams{
MaxTime: 100,
MaxSentCount: 6,
PerPage: 20,
})
require.NoError(t, err)
require.Len(t, pn, 1)
assert.Equal(t, p2.Id, pn[0].PostId)
})
t.Run("Delete By Channel", func(t *testing.T) {
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.CreateAt = 10
p1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(true),
},
}
p2 := model.Post{}
p2.ChannelId = p1.ChannelId
p2.UserId = model.NewId()
p2.Message = NewTestId()
p2.CreateAt = 20
p2.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(true),
},
}
p3 := model.Post{}
p3.ChannelId = p1.ChannelId
p3.UserId = model.NewId()
p3.Message = NewTestId()
p3.CreateAt = 30
p3.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(true),
},
}
p4 := model.Post{}
p4.ChannelId = model.NewId()
p4.UserId = model.NewId()
p4.Message = NewTestId()
p4.CreateAt = 40
p4.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(true),
},
}
p5 := model.Post{}
p5.ChannelId = p4.ChannelId
p5.UserId = model.NewId()
p5.Message = NewTestId()
p5.CreateAt = 50
p5.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(true),
},
}
_, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2, &p3, &p4, &p5})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
defer ss.Post().PermanentDeleteByChannel(p1.ChannelId)
defer ss.Post().PermanentDeleteByChannel(p4.ChannelId)
defer ss.PostPersistentNotification().Delete([]string{p1.Id, p2.Id, p3.Id, p4.Id, p5.Id})
err = ss.PostPersistentNotification().DeleteByChannel([]string{p1.ChannelId})
require.NoError(t, err)
pn, err := ss.PostPersistentNotification().Get(model.GetPersistentNotificationsPostsParams{
MaxTime: 100,
MaxSentCount: 6,
PerPage: 20,
})
require.NoError(t, err)
require.Len(t, pn, 2)
assert.ElementsMatch(t, []string{p4.Id, p5.Id}, []string{pn[0].PostId, pn[1].PostId})
})
t.Run("Delete By Team", func(t *testing.T) {
t1 := &model.Team{DisplayName: "t1", Name: NewTestId(), Email: MakeEmail(), Type: model.TeamOpen}
_, err := ss.Team().Save(t1)
require.NoError(t, err)
t2 := &model.Team{DisplayName: "t2", Name: NewTestId(), Email: MakeEmail(), Type: model.TeamOpen}
_, err = ss.Team().Save(t2)
require.NoError(t, err)
c1 := &model.Channel{TeamId: t1.Id, Name: model.NewId(), DisplayName: "c1", Type: model.ChannelTypeOpen}
_, err = ss.Channel().Save(c1, -1)
require.NoError(t, err)
c2 := &model.Channel{TeamId: t1.Id, Name: model.NewId(), DisplayName: "c2", Type: model.ChannelTypeOpen}
_, err = ss.Channel().Save(c2, -1)
require.NoError(t, err)
c3 := &model.Channel{TeamId: t2.Id, Name: model.NewId(), DisplayName: "c1", Type: model.ChannelTypeOpen}
_, err = ss.Channel().Save(c3, -1)
require.NoError(t, err)
p1 := model.Post{}
p1.ChannelId = c1.Id
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.CreateAt = 10
p1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(true),
},
}
p2 := model.Post{}
p2.ChannelId = p1.ChannelId
p2.UserId = model.NewId()
p2.Message = NewTestId()
p2.CreateAt = 20
p2.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(true),
},
}
p3 := model.Post{}
p3.ChannelId = c2.Id
p3.UserId = model.NewId()
p3.Message = NewTestId()
p3.CreateAt = 30
p3.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(true),
},
}
p4 := model.Post{}
p4.ChannelId = c3.Id
p4.UserId = model.NewId()
p4.Message = NewTestId()
p4.CreateAt = 40
p4.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(true),
},
}
p5 := model.Post{}
p5.ChannelId = p4.ChannelId
p5.UserId = model.NewId()
p5.Message = NewTestId()
p5.CreateAt = 50
p5.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(true),
},
}
_, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2, &p3, &p4, &p5})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
defer ss.Post().PermanentDeleteByChannel(c1.Id)
defer ss.Post().PermanentDeleteByChannel(c2.Id)
defer ss.Post().PermanentDeleteByChannel(c3.Id)
defer ss.Channel().PermanentDeleteByTeam(t1.Id)
defer ss.Channel().PermanentDeleteByTeam(t2.Id)
defer ss.Team().PermanentDelete(t1.Id)
defer ss.Team().PermanentDelete(t2.Id)
defer ss.PostPersistentNotification().Delete([]string{p1.Id, p2.Id, p3.Id, p4.Id, p5.Id})
err = ss.PostPersistentNotification().DeleteByTeam([]string{t1.Id})
require.NoError(t, err)
pn, err := ss.PostPersistentNotification().Get(model.GetPersistentNotificationsPostsParams{
MaxTime: 100,
MaxSentCount: 6,
PerPage: 20,
})
require.NoError(t, err)
require.Len(t, pn, 2)
assert.ElementsMatch(t, []string{p4.Id, p5.Id}, []string{pn[0].PostId, pn[1].PostId})
})
}

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

@@ -17,49 +17,50 @@ import (
// Store can be used to provide mock stores for testing.
type Store struct {
TeamStore mocks.TeamStore
ChannelStore mocks.ChannelStore
PostStore mocks.PostStore
UserStore mocks.UserStore
RetentionPolicyStore mocks.RetentionPolicyStore
BotStore mocks.BotStore
AuditStore mocks.AuditStore
ClusterDiscoveryStore mocks.ClusterDiscoveryStore
RemoteClusterStore mocks.RemoteClusterStore
ComplianceStore mocks.ComplianceStore
SessionStore mocks.SessionStore
OAuthStore mocks.OAuthStore
SystemStore mocks.SystemStore
WebhookStore mocks.WebhookStore
CommandStore mocks.CommandStore
CommandWebhookStore mocks.CommandWebhookStore
PreferenceStore mocks.PreferenceStore
LicenseStore mocks.LicenseStore
TokenStore mocks.TokenStore
EmojiStore mocks.EmojiStore
ThreadStore mocks.ThreadStore
StatusStore mocks.StatusStore
FileInfoStore mocks.FileInfoStore
UploadSessionStore mocks.UploadSessionStore
ReactionStore mocks.ReactionStore
JobStore mocks.JobStore
UserAccessTokenStore mocks.UserAccessTokenStore
PluginStore mocks.PluginStore
ChannelMemberHistoryStore mocks.ChannelMemberHistoryStore
RoleStore mocks.RoleStore
SchemeStore mocks.SchemeStore
TermsOfServiceStore mocks.TermsOfServiceStore
GroupStore mocks.GroupStore
UserTermsOfServiceStore mocks.UserTermsOfServiceStore
LinkMetadataStore mocks.LinkMetadataStore
SharedChannelStore mocks.SharedChannelStore
ProductNoticesStore mocks.ProductNoticesStore
DraftStore mocks.DraftStore
context context.Context
NotifyAdminStore mocks.NotifyAdminStore
PostPriorityStore mocks.PostPriorityStore
PostAcknowledgementStore mocks.PostAcknowledgementStore
TrueUpReviewStore mocks.TrueUpReviewStore
TeamStore mocks.TeamStore
ChannelStore mocks.ChannelStore
PostStore mocks.PostStore
UserStore mocks.UserStore
RetentionPolicyStore mocks.RetentionPolicyStore
BotStore mocks.BotStore
AuditStore mocks.AuditStore
ClusterDiscoveryStore mocks.ClusterDiscoveryStore
RemoteClusterStore mocks.RemoteClusterStore
ComplianceStore mocks.ComplianceStore
SessionStore mocks.SessionStore
OAuthStore mocks.OAuthStore
SystemStore mocks.SystemStore
WebhookStore mocks.WebhookStore
CommandStore mocks.CommandStore
CommandWebhookStore mocks.CommandWebhookStore
PreferenceStore mocks.PreferenceStore
LicenseStore mocks.LicenseStore
TokenStore mocks.TokenStore
EmojiStore mocks.EmojiStore
ThreadStore mocks.ThreadStore
StatusStore mocks.StatusStore
FileInfoStore mocks.FileInfoStore
UploadSessionStore mocks.UploadSessionStore
ReactionStore mocks.ReactionStore
JobStore mocks.JobStore
UserAccessTokenStore mocks.UserAccessTokenStore
PluginStore mocks.PluginStore
ChannelMemberHistoryStore mocks.ChannelMemberHistoryStore
RoleStore mocks.RoleStore
SchemeStore mocks.SchemeStore
TermsOfServiceStore mocks.TermsOfServiceStore
GroupStore mocks.GroupStore
UserTermsOfServiceStore mocks.UserTermsOfServiceStore
LinkMetadataStore mocks.LinkMetadataStore
SharedChannelStore mocks.SharedChannelStore
ProductNoticesStore mocks.ProductNoticesStore
DraftStore mocks.DraftStore
context context.Context
NotifyAdminStore mocks.NotifyAdminStore
PostPriorityStore mocks.PostPriorityStore
PostAcknowledgementStore mocks.PostAcknowledgementStore
PostPersistentNotificationStore mocks.PostPersistentNotificationStore
TrueUpReviewStore mocks.TrueUpReviewStore
}
func (s *Store) SetContext(context context.Context) { s.context = context }
@@ -110,6 +111,9 @@ func (s *Store) PostPriority() store.PostPriorityStore { return &s.PostPriorit
func (s *Store) PostAcknowledgement() store.PostAcknowledgementStore {
return &s.PostAcknowledgementStore
}
func (s *Store) PostPersistentNotification() store.PostPersistentNotificationStore {
return &s.PostPersistentNotificationStore
}
func (s *Store) MarkSystemRanUnitTests() { /* do nothing */ }
func (s *Store) Close() { /* do nothing */ }
func (s *Store) LockToMaster() { /* do nothing */ }
@@ -171,5 +175,6 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool {
&s.NotifyAdminStore,
&s.PostPriorityStore,
&s.PostAcknowledgementStore,
&s.PostPersistentNotificationStore,
)
}

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

@@ -17,49 +17,50 @@ import (
type TimerLayer struct {
store.Store
Metrics einterfaces.MetricsInterface
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
ChannelMemberHistoryStore store.ChannelMemberHistoryStore
ClusterDiscoveryStore store.ClusterDiscoveryStore
CommandStore store.CommandStore
CommandWebhookStore store.CommandWebhookStore
ComplianceStore store.ComplianceStore
DraftStore store.DraftStore
EmojiStore store.EmojiStore
FileInfoStore store.FileInfoStore
GroupStore store.GroupStore
JobStore store.JobStore
LicenseStore store.LicenseStore
LinkMetadataStore store.LinkMetadataStore
NotifyAdminStore store.NotifyAdminStore
OAuthStore store.OAuthStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
ReactionStore store.ReactionStore
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
RoleStore store.RoleStore
SchemeStore store.SchemeStore
SessionStore store.SessionStore
SharedChannelStore store.SharedChannelStore
StatusStore store.StatusStore
SystemStore store.SystemStore
TeamStore store.TeamStore
TermsOfServiceStore store.TermsOfServiceStore
ThreadStore store.ThreadStore
TokenStore store.TokenStore
TrueUpReviewStore store.TrueUpReviewStore
UploadSessionStore store.UploadSessionStore
UserStore store.UserStore
UserAccessTokenStore store.UserAccessTokenStore
UserTermsOfServiceStore store.UserTermsOfServiceStore
WebhookStore store.WebhookStore
Metrics einterfaces.MetricsInterface
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
ChannelMemberHistoryStore store.ChannelMemberHistoryStore
ClusterDiscoveryStore store.ClusterDiscoveryStore
CommandStore store.CommandStore
CommandWebhookStore store.CommandWebhookStore
ComplianceStore store.ComplianceStore
DraftStore store.DraftStore
EmojiStore store.EmojiStore
FileInfoStore store.FileInfoStore
GroupStore store.GroupStore
JobStore store.JobStore
LicenseStore store.LicenseStore
LinkMetadataStore store.LinkMetadataStore
NotifyAdminStore store.NotifyAdminStore
OAuthStore store.OAuthStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
PostPersistentNotificationStore store.PostPersistentNotificationStore
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
ReactionStore store.ReactionStore
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
RoleStore store.RoleStore
SchemeStore store.SchemeStore
SessionStore store.SessionStore
SharedChannelStore store.SharedChannelStore
StatusStore store.StatusStore
SystemStore store.SystemStore
TeamStore store.TeamStore
TermsOfServiceStore store.TermsOfServiceStore
ThreadStore store.ThreadStore
TokenStore store.TokenStore
TrueUpReviewStore store.TrueUpReviewStore
UploadSessionStore store.UploadSessionStore
UserStore store.UserStore
UserAccessTokenStore store.UserAccessTokenStore
UserTermsOfServiceStore store.UserTermsOfServiceStore
WebhookStore store.WebhookStore
}
func (s *TimerLayer) Audit() store.AuditStore {
@@ -142,6 +143,10 @@ func (s *TimerLayer) PostAcknowledgement() store.PostAcknowledgementStore {
return s.PostAcknowledgementStore
}
func (s *TimerLayer) PostPersistentNotification() store.PostPersistentNotificationStore {
return s.PostPersistentNotificationStore
}
func (s *TimerLayer) PostPriority() store.PostPriorityStore {
return s.PostPriorityStore
}
@@ -330,6 +335,11 @@ type TimerLayerPostAcknowledgementStore struct {
Root *TimerLayer
}
type TimerLayerPostPersistentNotificationStore struct {
store.PostPersistentNotificationStore
Root *TimerLayer
}
type TimerLayerPostPriorityStore struct {
store.PostPriorityStore
Root *TimerLayer
@@ -6186,6 +6196,118 @@ func (s *TimerLayerPostAcknowledgementStore) Save(postID string, userID string,
return result, err
}
func (s *TimerLayerPostPersistentNotificationStore) Delete(postIds []string) error {
start := time.Now()
err := s.PostPersistentNotificationStore.Delete(postIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostPersistentNotificationStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerPostPersistentNotificationStore) DeleteByChannel(channelIds []string) error {
start := time.Now()
err := s.PostPersistentNotificationStore.DeleteByChannel(channelIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostPersistentNotificationStore.DeleteByChannel", success, elapsed)
}
return err
}
func (s *TimerLayerPostPersistentNotificationStore) DeleteByTeam(teamIds []string) error {
start := time.Now()
err := s.PostPersistentNotificationStore.DeleteByTeam(teamIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostPersistentNotificationStore.DeleteByTeam", success, elapsed)
}
return err
}
func (s *TimerLayerPostPersistentNotificationStore) DeleteExpired(maxSentCount int16) error {
start := time.Now()
err := s.PostPersistentNotificationStore.DeleteExpired(maxSentCount)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostPersistentNotificationStore.DeleteExpired", success, elapsed)
}
return err
}
func (s *TimerLayerPostPersistentNotificationStore) Get(params model.GetPersistentNotificationsPostsParams) ([]*model.PostPersistentNotifications, error) {
start := time.Now()
result, err := s.PostPersistentNotificationStore.Get(params)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostPersistentNotificationStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostPersistentNotificationStore) GetSingle(postID string) (*model.PostPersistentNotifications, error) {
start := time.Now()
result, err := s.PostPersistentNotificationStore.GetSingle(postID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostPersistentNotificationStore.GetSingle", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostPersistentNotificationStore) UpdateLastActivity(postIds []string) error {
start := time.Now()
err := s.PostPersistentNotificationStore.UpdateLastActivity(postIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostPersistentNotificationStore.UpdateLastActivity", success, elapsed)
}
return err
}
func (s *TimerLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
start := time.Now()
@@ -11663,6 +11785,7 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay
newStore.PluginStore = &TimerLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
newStore.PostStore = &TimerLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
newStore.PostAcknowledgementStore = &TimerLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore}
newStore.PostPersistentNotificationStore = &TimerLayerPostPersistentNotificationStore{PostPersistentNotificationStore: childStore.PostPersistentNotification(), Root: &newStore}
newStore.PostPriorityStore = &TimerLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore}
newStore.PreferenceStore = &TimerLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
newStore.ProductNoticesStore = &TimerLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}

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

@@ -229,13 +229,13 @@ func RoundOffToZeroes(n float64) int64 {
return firstDigit * tens
}
func min(a, b int) int {
func MinInt(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
func MaxInt(a, b int) int {
if a > b {
return a
}
@@ -246,7 +246,7 @@ func max(a, b int) int {
// It implicitly sets the lowest minResolution to 0.
// e.g. 0 reports 1s, 1 reports 10s, 2 reports 100s, 3 reports 1000s
func RoundOffToZeroesResolution(n float64, minResolution int) int64 {
resolution := max(0, minResolution)
resolution := MaxInt(0, minResolution)
if n >= -9 && n <= 9 {
if resolution == 0 {
return int64(n)
@@ -255,7 +255,7 @@ func RoundOffToZeroesResolution(n float64, minResolution int) int64 {
}
zeroes := int(math.Log10(math.Abs(n)))
resolution = min(zeroes, resolution)
resolution = MinInt(zeroes, resolution)
tens := int64(math.Pow10(resolution))
significantDigits := int64(n) / tens
return significantDigits * tens

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

@@ -136,6 +136,11 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
props["EnableCustomGroups"] = "false"
props["InsightsEnabled"] = strconv.FormatBool(c.FeatureFlags.InsightsEnabled)
props["PostPriority"] = strconv.FormatBool(*c.ServiceSettings.PostPriority)
props["AllowPersistentNotifications"] = strconv.FormatBool(*c.ServiceSettings.AllowPersistentNotifications)
props["AllowPersistentNotificationsForGuests"] = strconv.FormatBool(*c.ServiceSettings.AllowPersistentNotificationsForGuests)
props["PersistentNotificationMaxCount"] = strconv.FormatInt(int64(*c.ServiceSettings.PersistentNotificationMaxCount), 10)
props["PersistentNotificationIntervalMinutes"] = strconv.FormatInt(int64(*c.ServiceSettings.PersistentNotificationIntervalMinutes), 10)
props["PersistentNotificationMaxRecipients"] = strconv.FormatInt(int64(*c.ServiceSettings.PersistentNotificationMaxRecipients), 10)
props["AllowSyncedDrafts"] = strconv.FormatBool(*c.ServiceSettings.AllowSyncedDrafts)
props["DelayChannelAutocomplete"] = strconv.FormatBool(*c.ExperimentalSettings.DelayChannelAutocomplete)

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

@@ -2375,6 +2375,30 @@
"id": "api.post.patch_post.can_not_update_post_in_deleted.error",
"translation": "Can not update a post in a deleted channel."
},
{
"id": "api.post.post_priority.max_recipients_persistent_notification_post.request_error",
"translation": "Persistent notification post allows maximum of {{.MaxRecipients}} recipients."
},
{
"id": "api.post.post_priority.min_recipients_persistent_notification_post.request_error",
"translation": "Persistent notification post must have minimum 1 recipient."
},
{
"id": "api.post.post_priority.persistent_notification_validation_error.request_error",
"translation": "Persistent notification validation failed."
},
{
"id": "api.post.post_priority.priority_post_not_allowed_for_user.request_error",
"translation": "User is not allowed to create priority post or persistent notification."
},
{
"id": "api.post.post_priority.priority_post_only_allowed_for_root_post.request_error",
"translation": "Only root posts are allowed to have priority."
},
{
"id": "api.post.post_priority.urgent_persistent_notification_post.request_error",
"translation": "Persistent notification posts must have the Urgent Priority."
},
{
"id": "api.post.posts_by_ids.invalid_body.request_error",
"translation": "The number of Post IDs received has exceeded the maximum size of {{.MaxLength}}"
@@ -6259,6 +6283,18 @@
"id": "app.post.update.app_error",
"translation": "Unable to update the Post."
},
{
"id": "app.post_persistent_notification.delete_by_channel.app_error",
"translation": "Unable to delete the persistent notifications by channel."
},
{
"id": "app.post_persistent_notification.delete_by_team.app_error",
"translation": "Unable to delete the persistent notifications by team."
},
{
"id": "app.post_priority.delete_persistent_notification_post.app_error",
"translation": "Failed to delete persistent notification post"
},
{
"id": "app.post_prority.get_for_post.app_error",
"translation": "Unable to get postpriority for post"
@@ -8907,6 +8943,18 @@
"id": "model.config.is_valid.password_length.app_error",
"translation": "Minimum password length must be a whole number greater than or equal to {{.MinLength}} and less than or equal to {{.MaxLength}}."
},
{
"id": "model.config.is_valid.persistent_notifications_count.app_error",
"translation": "Invalid total number of persistent notification per post. Must be a positive number."
},
{
"id": "model.config.is_valid.persistent_notifications_interval.app_error",
"translation": "Invalid frequency of persistent notifications. Must be at least two minutes."
},
{
"id": "model.config.is_valid.persistent_notifications_recipients.app_error",
"translation": "Invalid maximum number of recipients for persistent notifications. Must be a positive number."
},
{
"id": "model.config.is_valid.rate_mem.app_error",
"translation": "Invalid memory store size for rate limit settings. Must be a positive number."

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

@@ -474,6 +474,11 @@ func (ts *TelemetryService) trackConfig() {
"restrict_link_previews": isDefault(*cfg.ServiceSettings.RestrictLinkPreviews, ""),
"enable_custom_groups": *cfg.ServiceSettings.EnableCustomGroups,
"post_priority": *cfg.ServiceSettings.PostPriority,
"allow_persistent_notifications": *cfg.ServiceSettings.AllowPersistentNotifications,
"allow_persistent_notifications_for_guests": *cfg.ServiceSettings.AllowPersistentNotificationsForGuests,
"persistent_notification_interval_minutes": *cfg.ServiceSettings.PersistentNotificationIntervalMinutes,
"persistent_notification_max_count": *cfg.ServiceSettings.PersistentNotificationMaxCount,
"persistent_notification_max_recipients": *cfg.ServiceSettings.PersistentNotificationMaxRecipients,
"self_hosted_purchase": *cfg.ServiceSettings.SelfHostedPurchase,
"allow_synced_drafts": *cfg.ServiceSettings.AllowSyncedDrafts,
})

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

@@ -376,6 +376,11 @@ type ServiceSettings struct {
EnableLatex *bool `access:"site_posts"`
EnableInlineLatex *bool `access:"site_posts"`
PostPriority *bool `access:"site_posts"`
AllowPersistentNotifications *bool `access:"site_posts"`
AllowPersistentNotificationsForGuests *bool `access:"site_posts"`
PersistentNotificationIntervalMinutes *int `access:"site_posts"`
PersistentNotificationMaxCount *int `access:"site_posts"`
PersistentNotificationMaxRecipients *int `access:"site_posts"`
EnableAPIChannelDeletion *bool
EnableLocalMode *bool `access:"cloud_restrictable"`
LocalModeSocketLocation *string `access:"cloud_restrictable"` // telemetry: none
@@ -854,6 +859,26 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) {
s.PostPriority = NewBool(true)
}
if s.AllowPersistentNotifications == nil {
s.AllowPersistentNotifications = NewBool(true)
}
if s.AllowPersistentNotificationsForGuests == nil {
s.AllowPersistentNotificationsForGuests = NewBool(false)
}
if s.PersistentNotificationIntervalMinutes == nil {
s.PersistentNotificationIntervalMinutes = NewInt(5)
}
if s.PersistentNotificationMaxCount == nil {
s.PersistentNotificationMaxCount = NewInt(6)
}
if s.PersistentNotificationMaxRecipients == nil {
s.PersistentNotificationMaxRecipients = NewInt(5)
}
if s.AllowSyncedDrafts == nil {
s.AllowSyncedDrafts = NewBool(true)
}
@@ -3789,6 +3814,16 @@ func (s *ServiceSettings) isValid() *AppError {
return NewAppError("Config.IsValid", "model.config.is_valid.collapsed_threads.app_error", nil, "", http.StatusBadRequest)
}
if *s.PersistentNotificationIntervalMinutes < 2 {
return NewAppError("Config.IsValid", "model.config.is_valid.persistent_notifications_interval.app_error", nil, "", http.StatusBadRequest)
}
if *s.PersistentNotificationMaxCount <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.persistent_notifications_count.app_error", nil, "", http.StatusBadRequest)
}
if *s.PersistentNotificationMaxRecipients <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.persistent_notifications_recipients.app_error", nil, "", http.StatusBadRequest)
}
// we check if file has a valid parent, the server will try to create the socket
// file if it doesn't exist, but we need to be sure if the directory exist or not
if *s.EnableLocalMode {

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

@@ -32,6 +32,7 @@ const (
JobTypeLastAccessibleFile = "last_accessible_file"
JobTypeUpgradeNotifyAdmin = "upgrade_notify_admin"
JobTypeTrialNotifyAdmin = "trial_notify_admin"
JobTypePostPersistentNotifications = "post_persistent_notifications"
JobTypeInstallPluginNotifyAdmin = "install_plugin_notify_admin"
JobTypeHostedPurchaseScreening = "hosted_purchase_screening"

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

@@ -172,6 +172,20 @@ type PostPriority struct {
ChannelId string `json:",omitempty"`
}
type PostPersistentNotifications struct {
PostId string
CreateAt int64
LastSentAt int64
DeleteAt int64
SentCount int16
}
type GetPersistentNotificationsPostsParams struct {
MaxTime int64
MaxSentCount int16
PerPage int
}
type SearchParameter struct {
Terms *string `json:"terms"`
IsOrSearch *bool `json:"is_or_search"`
@@ -803,11 +817,26 @@ func (o *Post) GetPreviewedPostProp() string {
}
func (o *Post) GetPriority() *PostPriority {
if o.Metadata != nil && o.Metadata.Priority != nil {
return o.Metadata.Priority
if o.Metadata == nil {
return nil
}
return o.Metadata.Priority
}
return nil
func (o *Post) GetPersistentNotification() *bool {
priority := o.GetPriority()
if priority == nil {
return nil
}
return priority.PersistentNotifications
}
func (o *Post) GetRequestedAck() *bool {
priority := o.GetPriority()
if priority == nil {
return nil
}
return priority.RequestedAck
}
func (o *Post) IsUrgent() bool {

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

@@ -81,6 +81,7 @@ const (
WebsocketEventDraftDeleted = "draft_deleted"
WebsocketEventAcknowledgementAdded = "post_acknowledgement_added"
WebsocketEventAcknowledgementRemoved = "post_acknowledgement_removed"
WebsocketEventPersistentNotificationTriggered = "persistent_notification_triggered"
WebsocketEventHostedCustomerSignupProgressUpdated = "hosted_customer_signup_progress_updated"
)