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

* MM-46410: adds urgency on mention counts

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

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

* Adds PostAcknowledgements table and apis

* job init and fetch mentions

* add-migrations

* delete-expired

* send-notifications

* Fetches post priority in batches

* stop-notifications

* stop-notification-on-reply

* MM-47750: Adds PostAcknowledgements table and apis

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

* add-license-check

* add-pagination

* delete on channel and team

* validate guests

* add configs

* move create priority post check from app to api

* Add desktop notifications

* check status

* use config in job

* add IsUrgent check

* Add last-sent-at

* validate max recipients

* Update lastSentAt

* Validate min. recipient

* send email notification only once

* remove email notifications

* use latest time from config to run job

* Add notifications counter

* publish events to mentioned users only

* pickup license updates in scheduler

* don't allow post owner to stop notifications

* follow normal notifications behaviour

* Validates persistent notifications interval

* move logic of handling valid and expired posts into sql

* Adds persistent notifications in the webapp

---------

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

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

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