Files
upstream-mattermost/server/channels/app/delivery_tracking.go
upstream-mirror 240db4e2a7
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:29:55 +05:00

241 lines
8.4 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
)
func (a *App) deliveryTrackingEnabled() bool {
cfg := a.Config()
return cfg.FeatureFlags != nil && cfg.FeatureFlags.PostDeliveryTracking &&
model.SafeDereference(cfg.DeliveryTrackingSettings.Enable)
}
func shouldTrackPost(post *model.Post) bool {
return post != nil && post.Id != "" && !post.IsSystemMessage() && post.Type != model.PostTypeBurnOnRead && post.Type != model.PostTypeEphemeral
}
func (a *App) shouldTrackChannel(rctx request.CTX, channelID string) bool {
if channelID == "" {
return false
}
deliveryTracking := a.Srv().Store().DeliveryTracking()
// An explicit allow-list is validated DM/GM-free when it is saved, so membership alone
// settles eligibility and no channel has to be resolved.
if !model.SafeDereference(a.Config().DeliveryTrackingSettings.EnableForAllChannels) {
tracked, err := deliveryTracking.IsChannelTracked(rctx, channelID)
if err != nil {
rctx.Logger().Warn("Failed to read tracked channel for post delivery tracking", mlog.String("channel_id", channelID), mlog.Err(err))
return false
}
return tracked
}
trackable, err := deliveryTracking.IsChannelTrackable(rctx, channelID)
if err != nil {
rctx.Logger().Warn("Failed to read channel eligibility for post delivery tracking", mlog.String("channel_id", channelID), mlog.Err(err))
return false
}
return trackable
}
// emitDeliveryRecord writes one postDelivered record. actorUserID is the recipient, and is empty
// for plugin and outgoing webhook deliveries, which have no human recipient.
func (a *App) emitDeliveryRecord(actorUserID string, meta map[string]any) {
if a.Srv() == nil || a.Srv().Audit == nil {
return
}
a.Srv().Audit.LogRecord(mlog.LvlAuditDelivery, model.AuditRecord{
EventName: model.AuditEventPostDelivered,
Status: model.AuditStatusSuccess,
Actor: model.AuditEventActor{UserId: actorUserID},
Meta: meta,
})
}
func deliveryMeta(post *model.Post, mechanism string) map[string]any {
return map[string]any{
model.PostDeliveryKeyPostID: post.Id,
model.PostDeliveryKeyChannelID: post.ChannelId,
model.PostDeliveryKeyMechanism: mechanism,
}
}
func (a *App) emitPluginDeliveryRecord(pluginID string, post *model.Post) {
meta := deliveryMeta(post, model.DeliveryMechanismPlugin)
meta[model.PostDeliveryKeyPluginID] = pluginID
a.emitDeliveryRecord("", meta)
}
func (a *App) recordPostDelivery(rctx request.CTX, userID string, post *model.Post, mechanism string) {
if userID == "" || !shouldTrackPost(post) || post.UserId == userID || !a.shouldTrackChannel(rctx, post.ChannelId) {
return
}
a.emitDeliveryRecord(userID, deliveryMeta(post, mechanism))
}
func (a *App) recordPostDeliveryToPlugin(rctx request.CTX, pluginID string, post *model.Post) {
if !shouldTrackPost(post) || !a.shouldTrackChannel(rctx, post.ChannelId) {
return
}
a.emitPluginDeliveryRecord(pluginID, post)
}
// RecordPostDelivery, RecordPostsDelivery and RecordPostListDelivery record that a post's content
// reached userID. Request handlers must call these only after the response body was written
// successfully, so that a failed write doesn't claim a delivery that never happened.
func (a *App) RecordPostDelivery(rctx request.CTX, userID string, post *model.Post, mechanism string) {
if !a.deliveryTrackingEnabled() {
return
}
a.recordPostDelivery(rctx, userID, post, mechanism)
}
func (a *App) RecordPostsDelivery(rctx request.CTX, userID string, posts []*model.Post, mechanism string) {
if !a.deliveryTrackingEnabled() || userID == "" {
return
}
for _, post := range posts {
a.recordPostDelivery(rctx, userID, post, mechanism)
}
}
func (a *App) RecordPostListDelivery(rctx request.CTX, userID string, list *model.PostList, mechanism string) {
if !a.deliveryTrackingEnabled() || userID == "" || list == nil {
return
}
for _, post := range list.Posts {
a.recordPostDelivery(rctx, userID, post, mechanism)
}
}
// RecordPermalinkPreviewDelivery records that a previewed post's content reached a user inside
// containingPost. The previewed post's channel governs: the delivery is recorded even when the
// containing post lives in a channel that is not tracked, otherwise a tracked post could be read
// by permalinking it into any other channel.
func (a *App) RecordPermalinkPreviewDelivery(rctx request.CTX, userID string, previewedPost, containingPost *model.Post) {
if !a.deliveryTrackingEnabled() || userID == "" || !shouldTrackPost(previewedPost) ||
previewedPost.UserId == userID || !a.shouldTrackChannel(rctx, previewedPost.ChannelId) {
return
}
meta := deliveryMeta(previewedPost, model.DeliveryMechanismPermalinkPreview)
if containingPost != nil {
meta[model.PostDeliveryKeyViaPostID] = containingPost.Id
meta[model.PostDeliveryKeyViaChannelID] = containingPost.ChannelId
}
a.emitDeliveryRecord(userID, meta)
}
// RecordBroadcastDelivery records a websocket delivery of the marked post to one connection's
// user. The marker is attached only to events whose post and channel already passed the tracking
// checks, so nothing is resolved here — this runs on the hub's goroutine.
func (a *App) RecordBroadcastDelivery(marker *model.PostDeliveryMarker, userID string) {
if marker == nil || marker.PostId == "" || userID == "" || marker.UserId == userID ||
!a.deliveryTrackingEnabled() {
return
}
a.emitDeliveryRecord(userID, map[string]any{
model.PostDeliveryKeyPostID: marker.PostId,
model.PostDeliveryKeyChannelID: marker.ChannelId,
model.PostDeliveryKeyMechanism: model.DeliveryMechanismPostBroadcast,
})
}
func (a *App) markPostDeliveryForBroadcast(rctx request.CTX, message *model.WebSocketEvent, post *model.Post) {
if !a.deliveryTrackingEnabled() || !shouldTrackPost(post) || !a.shouldTrackChannel(rctx, post.ChannelId) {
return
}
message.GetBroadcast().RecordPostDelivery = &model.PostDeliveryMarker{
PostId: post.Id,
ChannelId: post.ChannelId,
UserId: post.UserId,
}
}
// RecordPushDelivery records that a post's content reached a user inside a push notification
// payload. Only full-content message pushes carry the post body: generic pushes deliver no
// content, and an id-loaded push carries only ids — that delivery is recorded by the ack handler
// the device calls to fetch the message.
func (a *App) RecordPushDelivery(rctx request.CTX, userID string, msg *model.PushNotification) {
if !a.deliveryTrackingEnabled() || msg == nil || msg.Type != model.PushTypeMessage ||
msg.PostId == "" || *a.Config().EmailSettings.PushNotificationContents != model.FullNotification {
return
}
a.recordPostDelivery(rctx, userID, &model.Post{
Id: msg.PostId,
ChannelId: msg.ChannelId,
UserId: msg.SenderId,
Type: msg.PostType,
}, model.DeliveryMechanismPush)
}
func (a *App) RecordPostDeliveryToPlugin(rctx request.CTX, pluginID string, post *model.Post) {
if !a.deliveryTrackingEnabled() || pluginID == "" {
return
}
a.recordPostDeliveryToPlugin(rctx, pluginID, post)
}
func (a *App) RecordPostDeliveryToPlugins(rctx request.CTX, pluginIDs []string, post *model.Post) {
if !a.deliveryTrackingEnabled() || len(pluginIDs) == 0 || !shouldTrackPost(post) ||
!a.shouldTrackChannel(rctx, post.ChannelId) {
return
}
for _, pluginID := range pluginIDs {
if pluginID == "" {
continue
}
a.emitPluginDeliveryRecord(pluginID, post)
}
}
func (a *App) RecordPostsDeliveryToPlugin(rctx request.CTX, pluginID string, posts []*model.Post) {
if !a.deliveryTrackingEnabled() || pluginID == "" {
return
}
for _, post := range posts {
a.recordPostDeliveryToPlugin(rctx, pluginID, post)
}
}
func (a *App) RecordPostListDeliveryToPlugin(rctx request.CTX, pluginID string, list *model.PostList) {
if !a.deliveryTrackingEnabled() || pluginID == "" || list == nil {
return
}
for _, post := range list.Posts {
a.recordPostDeliveryToPlugin(rctx, pluginID, post)
}
}
func (a *App) RecordPostDeliveryToWebhook(rctx request.CTX, webhookID string, post *model.Post) {
if !a.deliveryTrackingEnabled() || webhookID == "" || !shouldTrackPost(post) ||
!a.shouldTrackChannel(rctx, post.ChannelId) {
return
}
meta := deliveryMeta(post, model.DeliveryMechanismOutgoingWebhook)
meta[model.PostDeliveryKeyWebhookID] = webhookID
a.emitDeliveryRecord("", meta)
}