fix for retention logic
This commit is contained in:
parent
70c0ba44f0
commit
e6bfe2d444
35 changed files with 2108 additions and 132 deletions
204
internal/app/files/retention_purge.go
Normal file
204
internal/app/files/retention_purge.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package files
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// RetentionPurgeMessageEditor is the private-message capability the storage
|
||||
// retention sweep needs to turn a hard-retention/eviction blob purge into a
|
||||
// visible messageActionCustomAction notice. Satisfied by
|
||||
// internal/app/messages.Service (GetMessages resolves the box's peer --
|
||||
// EditMessageRequest requires it, and message_box ref_key only encodes
|
||||
// owner_user_id+box_id -- then EditMessage performs the actual in-place
|
||||
// edit with RetentionPurge set).
|
||||
type RetentionPurgeMessageEditor interface {
|
||||
GetMessages(ctx context.Context, userID int64, ids []int) (domain.MessageList, error)
|
||||
EditMessage(ctx context.Context, userID int64, req domain.EditMessageRequest) (domain.EditMessageResult, error)
|
||||
}
|
||||
|
||||
// RetentionPurgeChannelEditor is the channel-message counterpart of
|
||||
// RetentionPurgeMessageEditor. Satisfied by internal/app/channels.Service.
|
||||
// Unlike private messages, EditChannelMessageRequest needs no peer lookup --
|
||||
// channelMessageRefKey already encodes the channel id directly. Uses
|
||||
// EditChannelMessageInternal (not the ordinary EditMessage) since this is a
|
||||
// server-internal edit with no acting user/channel member behind it.
|
||||
type RetentionPurgeChannelEditor interface {
|
||||
EditChannelMessageInternal(ctx context.Context, req domain.EditChannelMessageRequest) (domain.EditChannelMessageResult, error)
|
||||
}
|
||||
|
||||
// SetRetentionPurgeNotifier wires the private-message/channel-message edit
|
||||
// capability the storage retention sweep needs to turn a hard-retention/
|
||||
// eviction blob purge into a visible messageActionCustomAction notice on
|
||||
// every message still embedding the purged document/photo. Both app-layer
|
||||
// services are constructed after this Service in cmd/telesrv/main.go (and
|
||||
// the background retention sweep goroutine is started before either exists),
|
||||
// so this is a post-construction setter rather than a NewService option: a
|
||||
// sweep tick that races ahead of this call simply finds both fields nil and
|
||||
// skips the notice for that tick, same as any other per-reference failure
|
||||
// below (best-effort -- the underlying blob purge has already committed and
|
||||
// must never be undone or retried because of a notice failure).
|
||||
func (s *Service) SetRetentionPurgeNotifier(messages RetentionPurgeMessageEditor, channels RetentionPurgeChannelEditor) {
|
||||
s.retentionNotifyMu.Lock()
|
||||
defer s.retentionNotifyMu.Unlock()
|
||||
s.retentionMessages = messages
|
||||
s.retentionChannels = channels
|
||||
}
|
||||
|
||||
func (s *Service) retentionNotifier() (RetentionPurgeMessageEditor, RetentionPurgeChannelEditor) {
|
||||
s.retentionNotifyMu.RLock()
|
||||
defer s.retentionNotifyMu.RUnlock()
|
||||
return s.retentionMessages, s.retentionChannels
|
||||
}
|
||||
|
||||
// notifyRetentionPurge turns a just-completed hard-retention/eviction blob
|
||||
// purge of a document/photo into a visible service-message notice on every
|
||||
// message that still embeds it. profile_photo/sticker_set/gift references
|
||||
// have no message to edit and are skipped. Best-effort throughout: a lookup
|
||||
// or edit failure is logged and never propagated -- the underlying blob purge
|
||||
// has already committed and must not be undone or retried because of this.
|
||||
func (s *Service) notifyRetentionPurge(ctx context.Context, kind domain.MediaKind, mediaID int64) {
|
||||
messages, channels := s.retentionNotifier()
|
||||
if messages == nil && channels == nil {
|
||||
return
|
||||
}
|
||||
store, ok := s.media.(mediaRetentionStore)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
refs, err := store.ListMediaReferences(ctx, kind, mediaID)
|
||||
if err != nil {
|
||||
s.log.Warn("list media references for retention purge notice failed",
|
||||
zap.String("media_kind", string(kind)), zap.Int64("media_id", mediaID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
for _, ref := range refs {
|
||||
switch ref.RefKind {
|
||||
case domain.MediaRefKindMessageBox:
|
||||
s.notifyRetentionPurgeMessageBox(ctx, messages, ref.RefKey)
|
||||
case domain.MediaRefKindChannelMessage:
|
||||
s.notifyRetentionPurgeChannelMessage(ctx, channels, ref.RefKey)
|
||||
case domain.MediaRefKindProfilePhoto, domain.MediaRefKindStickerSet, domain.MediaRefKindGift:
|
||||
// No message to edit for these ref kinds.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// notifyRetentionPurgeMessageBox parses a message_box ref_key (format
|
||||
// "user:<owner_user_id>:box:<box_id>", see postgres.messageBoxRefKey -- kept
|
||||
// in sync by convention, not a shared symbol, since the ref_key encoding is
|
||||
// an internal storage detail of that package) and edits the box in place.
|
||||
func (s *Service) notifyRetentionPurgeMessageBox(ctx context.Context, messages RetentionPurgeMessageEditor, refKey string) {
|
||||
if messages == nil {
|
||||
return
|
||||
}
|
||||
ownerUserID, boxID, ok := parseMessageBoxRefKey(refKey)
|
||||
if !ok {
|
||||
s.log.Warn("unparseable message_box retention purge ref_key", zap.String("ref_key", refKey))
|
||||
return
|
||||
}
|
||||
// EditMessageRequest requires the peer explicitly (an ordinary edit's RPC
|
||||
// caller always supplies it) -- resolve it from the box row itself since
|
||||
// this is a server-internal edit with no client request behind it.
|
||||
list, err := messages.GetMessages(ctx, ownerUserID, []int{boxID})
|
||||
if err != nil {
|
||||
s.log.Warn("resolve message_box peer for retention purge notice failed",
|
||||
zap.Int64("owner_user_id", ownerUserID), zap.Int("box_id", boxID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
if len(list.Messages) == 0 {
|
||||
// Box no longer visible to its owner (deleted) -- nothing to notice.
|
||||
return
|
||||
}
|
||||
peer := list.Messages[0].Peer
|
||||
_, err = messages.EditMessage(ctx, ownerUserID, domain.EditMessageRequest{
|
||||
OwnerUserID: ownerUserID,
|
||||
Peer: peer,
|
||||
ID: boxID,
|
||||
SetRichMessage: true,
|
||||
RichMessage: nil,
|
||||
Media: retentionPurgeNoticeMedia(),
|
||||
RetentionPurge: true,
|
||||
})
|
||||
if err != nil && !errors.Is(err, domain.ErrMessageNotModified) && !errors.Is(err, domain.ErrMessageIDInvalid) {
|
||||
s.log.Warn("retention purge notice edit failed (message_box)",
|
||||
zap.Int64("owner_user_id", ownerUserID), zap.Int("box_id", boxID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// notifyRetentionPurgeChannelMessage parses a channel_message ref_key
|
||||
// (format "channel:<channel_id>:msg:<message_id>", see
|
||||
// postgres.channelMessageRefKey) and edits the message in place. Unlike
|
||||
// private messages, no peer lookup is needed: EditChannelMessageRequest only
|
||||
// needs the channel id, already encoded in the ref_key.
|
||||
func (s *Service) notifyRetentionPurgeChannelMessage(ctx context.Context, channels RetentionPurgeChannelEditor, refKey string) {
|
||||
if channels == nil {
|
||||
return
|
||||
}
|
||||
channelID, messageID, ok := parseChannelMessageRefKey(refKey)
|
||||
if !ok {
|
||||
s.log.Warn("unparseable channel_message retention purge ref_key", zap.String("ref_key", refKey))
|
||||
return
|
||||
}
|
||||
_, err := channels.EditChannelMessageInternal(ctx, domain.EditChannelMessageRequest{
|
||||
ChannelID: channelID,
|
||||
ID: messageID,
|
||||
RetentionPurge: true,
|
||||
RetentionPurgeAction: &domain.ChannelMessageAction{Type: domain.ChannelActionCustomText, Text: domain.RetentionPurgeNoticeText},
|
||||
})
|
||||
if err != nil && !errors.Is(err, domain.ErrMessageNotModified) && !errors.Is(err, domain.ErrMessageIDInvalid) {
|
||||
s.log.Warn("retention purge notice edit failed (channel_message)",
|
||||
zap.Int64("channel_id", channelID), zap.Int("message_id", messageID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// retentionPurgeNoticeMedia builds the messageActionCustomAction service
|
||||
// media payload private-message edits replace a purged message's media with.
|
||||
func retentionPurgeNoticeMedia() *domain.MessageMedia {
|
||||
return &domain.MessageMedia{
|
||||
Kind: domain.MessageMediaKindService,
|
||||
ServiceAction: &domain.MessageServiceAction{
|
||||
Kind: domain.MessageServiceActionCustomText,
|
||||
Text: domain.RetentionPurgeNoticeText,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// parseMessageBoxRefKey parses "user:<owner_user_id>:box:<box_id>".
|
||||
// fmt.Sscanf reports success and silently ignores unconsumed trailing input
|
||||
// (e.g. "user:1:box:2garbage" would still scan fine), so a bare cnt/err check
|
||||
// isn't enough -- round-trip the parsed ids back through the same format
|
||||
// (messageBoxRefKey's own layout) and require an exact match to reject any
|
||||
// malformed/truncated/trailing-garbage key. Defensive only: ref_key is
|
||||
// server-written and never externally supplied, but a bug or future
|
||||
// encoding change should fail closed here, not silently misdirect an edit at
|
||||
// the wrong box.
|
||||
func parseMessageBoxRefKey(refKey string) (ownerUserID int64, boxID int, ok bool) {
|
||||
cnt, err := fmt.Sscanf(refKey, "user:%d:box:%d", &ownerUserID, &boxID)
|
||||
if err != nil || cnt != 2 || ownerUserID == 0 || boxID == 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
if fmt.Sprintf("user:%d:box:%d", ownerUserID, boxID) != refKey {
|
||||
return 0, 0, false
|
||||
}
|
||||
return ownerUserID, boxID, true
|
||||
}
|
||||
|
||||
// parseChannelMessageRefKey parses "channel:<channel_id>:msg:<message_id>".
|
||||
// See parseMessageBoxRefKey's doc comment -- same round-trip defense against
|
||||
// trailing-garbage input Sscanf alone would silently accept.
|
||||
func parseChannelMessageRefKey(refKey string) (channelID int64, messageID int, ok bool) {
|
||||
cnt, err := fmt.Sscanf(refKey, "channel:%d:msg:%d", &channelID, &messageID)
|
||||
if err != nil || cnt != 2 || channelID == 0 || messageID == 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
if fmt.Sprintf("channel:%d:msg:%d", channelID, messageID) != refKey {
|
||||
return 0, 0, false
|
||||
}
|
||||
return channelID, messageID, true
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue