[MM-57067][MM-57006][MM-57328] Acknowledge websocket POSTED events that may result in a notification on the web client (#26604)
* Acknowledge POSTED events that may result in a notification * Remove temporary metrics * Add Desktop App support * Merge'd * Add some tests * PR feedback * Rework window is focused check * Oops * Move mentions/followers ACK to posted ACK broadcast hook --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
b397f9ed92
Коммит
8589476229
@@ -676,6 +676,18 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
|
||||
useAddFollowersHook(message, notificationsForCRT.Desktop)
|
||||
}
|
||||
|
||||
// Collect user IDs of whom we want to acknowledge the websocket event for notification metrics
|
||||
usersToAck := []string{}
|
||||
for id, profile := range profileMap {
|
||||
userNotificationLevel := profile.NotifyProps[model.DesktopNotifyProp]
|
||||
channelNotificationLevel := channelMemberNotifyPropsMap[id][model.DesktopNotifyProp]
|
||||
|
||||
if ShouldAckWebsocketNotification(channel.Type, userNotificationLevel, channelNotificationLevel) {
|
||||
usersToAck = append(usersToAck, id)
|
||||
}
|
||||
}
|
||||
usePostedAckHook(message, post.UserId, channel.Type, usersToAck)
|
||||
|
||||
published, err := a.publishWebsocketEventForPermalinkPost(c, post, message)
|
||||
if err != nil {
|
||||
a.NotificationsLog().Error("Couldn't send websocket notification for permalink post",
|
||||
@@ -1680,3 +1692,20 @@ func shouldChannelMemberNotifyCRT(userNotifyProps model.StringMap, channelMember
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func ShouldAckWebsocketNotification(channelType model.ChannelType, userNotificationLevel, channelNotificationLevel string) bool {
|
||||
if channelNotificationLevel == model.ChannelNotifyAll {
|
||||
// Should ACK on if we notify for all messages in the channel
|
||||
return true
|
||||
} else if channelNotificationLevel == model.ChannelNotifyDefault && userNotificationLevel == model.UserNotifyAll {
|
||||
// Should ACK on if we notify for all messages and the channel settings are unchanged
|
||||
return true
|
||||
} else if channelType == model.ChannelTypeGroup &&
|
||||
((channelNotificationLevel == model.ChannelNotifyDefault && userNotificationLevel == model.UserNotifyMention) ||
|
||||
channelNotificationLevel == model.ChannelNotifyMention) {
|
||||
// Should ACK for group channels where default settings are in place (should be notified)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -3049,3 +3049,16 @@ func TestRemoveNotifications(t *testing.T) {
|
||||
require.Equal(t, int64(0), thread.UnreadReplies)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShouldAckWebsocketNotification(t *testing.T) {
|
||||
t.Run("should return true if channel notify level is ALL", func(t *testing.T) {
|
||||
assert.True(t, ShouldAckWebsocketNotification(model.ChannelTypeOpen, model.UserNotifyNone, model.ChannelNotifyAll))
|
||||
})
|
||||
t.Run("should return true if user notify level is ALL and the channel is unchanged", func(t *testing.T) {
|
||||
assert.True(t, ShouldAckWebsocketNotification(model.ChannelTypeOpen, model.UserNotifyAll, model.ChannelNotifyDefault))
|
||||
})
|
||||
t.Run("should return true if its a group channel, and the level is mention", func(t *testing.T) {
|
||||
assert.True(t, ShouldAckWebsocketNotification(model.ChannelTypeGroup, model.UserNotifyMention, model.ChannelNotifyDefault))
|
||||
assert.True(t, ShouldAckWebsocketNotification(model.ChannelTypeGroup, model.UserNotifyNone, model.ChannelNotifyMention))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,12 +16,14 @@ import (
|
||||
const (
|
||||
broadcastAddMentions = "add_mentions"
|
||||
broadcastAddFollowers = "add_followers"
|
||||
broadcastPostedAck = "posted_ack"
|
||||
)
|
||||
|
||||
func (s *Server) makeBroadcastHooks() map[string]platform.BroadcastHook {
|
||||
return map[string]platform.BroadcastHook{
|
||||
broadcastAddMentions: &addMentionsBroadcastHook{},
|
||||
broadcastAddFollowers: &addFollowersBroadcastHook{},
|
||||
broadcastPostedAck: &postedAckBroadcastHook{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +71,57 @@ func useAddFollowersHook(message *model.WebSocketEvent, followers model.StringAr
|
||||
})
|
||||
}
|
||||
|
||||
type postedAckBroadcastHook struct{}
|
||||
|
||||
func usePostedAckHook(message *model.WebSocketEvent, postedUserId string, channelType model.ChannelType, usersToNotify []string) {
|
||||
message.GetBroadcast().AddHook(broadcastPostedAck, map[string]any{
|
||||
"posted_user_id": postedUserId,
|
||||
"channel_type": channelType,
|
||||
"users": usersToNotify,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *postedAckBroadcastHook) Process(msg *platform.HookedWebSocketEvent, webConn *platform.WebConn, args map[string]any) error {
|
||||
// Add if we have mentions or followers
|
||||
// This works since we currently do have an order for broadcast hooks, but this probably should be reworked going forward
|
||||
if msg.Get("followers") != nil || msg.Get("mentions") != nil {
|
||||
msg.Add("should_ack", true)
|
||||
return nil
|
||||
}
|
||||
|
||||
postedUserId, err := getTypedArg[string](args, "posted_user_id")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid posted_user_id value passed to postedAckBroadcastHook")
|
||||
}
|
||||
|
||||
// Don't ACK your own posts
|
||||
if postedUserId == webConn.UserId {
|
||||
return nil
|
||||
}
|
||||
|
||||
channelType, err := getTypedArg[model.ChannelType](args, "channel_type")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid channel_type value passed to postedAckBroadcastHook")
|
||||
}
|
||||
|
||||
// Always ACK direct channels
|
||||
if channelType == model.ChannelTypeDirect {
|
||||
msg.Add("should_ack", true)
|
||||
return nil
|
||||
}
|
||||
|
||||
users, err := getTypedArg[model.StringArray](args, "users")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid users value passed to addFollowersBroadcastHook")
|
||||
}
|
||||
|
||||
if len(users) > 0 && slices.Contains(users, webConn.UserId) {
|
||||
msg.Add("should_ack", true)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTypedArg returns a correctly typed hook argument with the given key, reinterpreting the type using JSON encoding
|
||||
// if necessary. This is needed because broadcast hook args are JSON encoded in a multi-server environment, and any
|
||||
// type information is lost because those types aren't known at decode time.
|
||||
|
||||
@@ -83,6 +83,62 @@ func TestAddFollowersHook_Process(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostedAckHook_Process(t *testing.T) {
|
||||
hook := &postedAckBroadcastHook{}
|
||||
userID := model.NewId()
|
||||
webConn := &platform.WebConn{
|
||||
UserId: userID,
|
||||
}
|
||||
|
||||
t.Run("should ack if user is in the list of users to notify", func(t *testing.T) {
|
||||
msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, ""))
|
||||
|
||||
hook.Process(msg, webConn, map[string]any{
|
||||
"posted_user_id": model.NewId(),
|
||||
"channel_type": model.ChannelTypeOpen,
|
||||
"users": []string{userID},
|
||||
})
|
||||
|
||||
assert.True(t, msg.Event().GetData()["should_ack"].(bool))
|
||||
})
|
||||
|
||||
t.Run("should not ack if user is not in the list of users to notify", func(t *testing.T) {
|
||||
msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, ""))
|
||||
|
||||
hook.Process(msg, webConn, map[string]any{
|
||||
"posted_user_id": model.NewId(),
|
||||
"channel_type": model.ChannelTypeOpen,
|
||||
"users": []string{},
|
||||
})
|
||||
|
||||
assert.Nil(t, msg.Event().GetData()["should_ack"])
|
||||
})
|
||||
|
||||
t.Run("should not ack if you are the user who posted", func(t *testing.T) {
|
||||
msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, ""))
|
||||
|
||||
hook.Process(msg, webConn, map[string]any{
|
||||
"posted_user_id": userID,
|
||||
"channel_type": model.ChannelTypeOpen,
|
||||
"users": []string{userID},
|
||||
})
|
||||
|
||||
assert.Nil(t, msg.Event().GetData()["should_ack"])
|
||||
})
|
||||
|
||||
t.Run("should ack if the channel is a DM", func(t *testing.T) {
|
||||
msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, ""))
|
||||
|
||||
hook.Process(msg, webConn, map[string]any{
|
||||
"posted_user_id": model.NewId(),
|
||||
"channel_type": model.ChannelTypeDirect,
|
||||
"users": []string{},
|
||||
})
|
||||
|
||||
assert.True(t, msg.Event().GetData()["should_ack"].(bool))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAddMentionsAndAddFollowersHooks(t *testing.T) {
|
||||
addMentionsHook := &addMentionsBroadcastHook{}
|
||||
addFollowersHook := &addFollowersBroadcastHook{}
|
||||
|
||||
@@ -5,10 +5,12 @@ package wsapi
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitSystem() {
|
||||
api.Router.Handle("ping", api.APIWebSocketHandler(ping))
|
||||
api.Router.Handle(model.WebsocketPostedNotifyAck, api.APIWebSocketHandler(api.websocketNotificationAck))
|
||||
}
|
||||
|
||||
func ping(req *model.WebSocketRequest) (map[string]any, *model.AppError) {
|
||||
@@ -20,3 +22,18 @@ func ping(req *model.WebSocketRequest) (map[string]any, *model.AppError) {
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (api *API) websocketNotificationAck(req *model.WebSocketRequest) (map[string]any, *model.AppError) {
|
||||
// Log the ACKs if necessary
|
||||
api.App.NotificationsLog().Debug("Websocket notification acknowledgment",
|
||||
mlog.String("type", model.TypeWebsocket),
|
||||
mlog.String("user_id", req.Session.UserId),
|
||||
mlog.Any("user_agent", req.Data["user_agent"]),
|
||||
mlog.Any("post_id", req.Data["post_id"]),
|
||||
mlog.Any("result", req.Data["result"]),
|
||||
mlog.Any("reason", req.Data["reason"]),
|
||||
mlog.Any("data", req.Data["data"]),
|
||||
)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ const (
|
||||
WebsocketEventChannelBookmarkDeleted = "channel_bookmark_deleted"
|
||||
WebsocketEventChannelBookmarkSorted = "channel_bookmark_sorted"
|
||||
WebsocketPresenceIndicator WebsocketEventType = "presence"
|
||||
WebsocketPostedNotifyAck = "posted_notify_ack"
|
||||
)
|
||||
|
||||
type WebSocketMessage interface {
|
||||
|
||||
Ссылка в новой задаче
Block a user