[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>
Этот коммит содержится в:
@@ -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))
|
||||
|
||||
396
server/channels/app/post_persistent_notification.go
Обычный файл
396
server/channels/app/post_persistent_notification.go
Обычный файл
@@ -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
|
||||
}
|
||||
237
server/channels/app/post_persistent_notification_test.go
Обычный файл
237
server/channels/app/post_persistent_notification_test.go
Обычный файл
@@ -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
|
||||
);
|
||||
28
server/channels/jobs/post_persistent_notifications/scheduler.go
Обычный файл
28
server/channels/jobs/post_persistent_notifications/scheduler.go
Обычный файл
@@ -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)}
|
||||
}
|
||||
30
server/channels/jobs/post_persistent_notifications/worker.go
Обычный файл
30
server/channels/jobs/post_persistent_notifications/worker.go
Обычный файл
@@ -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
|
||||
}
|
||||
|
||||
193
server/channels/store/sqlstore/post_persistent_notification_store.go
Обычный файл
193
server/channels/store/sqlstore/post_persistent_notification_store.go
Обычный файл
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user