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

* MM-46410: adds urgency on mention counts

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

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

* Adds PostAcknowledgements table and apis

* job init and fetch mentions

* add-migrations

* delete-expired

* send-notifications

* Fetches post priority in batches

* stop-notifications

* stop-notification-on-reply

* MM-47750: Adds PostAcknowledgements table and apis

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

* add-license-check

* add-pagination

* delete on channel and team

* validate guests

* add configs

* move create priority post check from app to api

* Add desktop notifications

* check status

* use config in job

* add IsUrgent check

* Add last-sent-at

* validate max recipients

* Update lastSentAt

* Validate min. recipient

* send email notification only once

* remove email notifications

* use latest time from config to run job

* Add notifications counter

* publish events to mentioned users only

* pickup license updates in scheduler

* don't allow post owner to stop notifications

* follow normal notifications behaviour

* Validates persistent notifications interval

* move logic of handling valid and expired posts into sql

* Adds persistent notifications in the webapp

---------

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

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

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

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

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

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

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