[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>
Этот коммит содержится в:
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
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user