perf: sync protocol and core hardening updates
This commit is contained in:
parent
152fed3b87
commit
4390ebf5a9
283 changed files with 29231 additions and 2295 deletions
28
internal/app/messages/album_group.go
Normal file
28
internal/app/messages/album_group.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package messages
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
// ReserveAlbumGroup 把 RPC 已验证的一批 album item 交给持久层原子预留。
|
||||
// 该能力只传 domain DTO;上传媒体解析与 tg 类型仍停留在 RPC edge。
|
||||
func (s *Service) ReserveAlbumGroup(ctx context.Context, userID int64, req domain.AlbumGroupReservationRequest) (int64, error) {
|
||||
if s == nil || s.messages == nil || userID <= 0 {
|
||||
return 0, domain.ErrAlbumGroupReservationInvalid
|
||||
}
|
||||
if req.SenderUserID == 0 {
|
||||
req.SenderUserID = userID
|
||||
}
|
||||
if req.SenderUserID != userID {
|
||||
return 0, domain.ErrAlbumGroupReservationInvalid
|
||||
}
|
||||
reservations, ok := s.messages.(store.AlbumGroupStore)
|
||||
if !ok {
|
||||
return 0, errors.New("message store does not support album group reservations")
|
||||
}
|
||||
return reservations.ReserveAlbumGroup(ctx, req)
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package messages
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"telesrv/internal/app/userprojection"
|
||||
"telesrv/internal/domain"
|
||||
|
|
@ -96,6 +97,28 @@ func (s *Service) SendPrivateText(ctx context.Context, userID int64, req domain.
|
|||
if req.SenderUserID == 0 {
|
||||
req.SenderUserID = userID
|
||||
}
|
||||
if req.SenderUserID != userID {
|
||||
return domain.SendPrivateTextResult{}, domain.ErrUserSendRestricted
|
||||
}
|
||||
if req.RandomID != 0 && !req.IdempotencyPreflighted {
|
||||
fingerprint, err := store.PrivateSendFingerprint(req)
|
||||
if err != nil {
|
||||
return domain.SendPrivateTextResult{}, err
|
||||
}
|
||||
req.IdempotencyFingerprint = fingerprint
|
||||
if replayStore, ok := s.messages.(store.PrivateSendReplayStore); ok {
|
||||
replay, found, err := replayStore.LookupPrivateSendReplay(ctx, domain.PrivateSendReplayRequest{
|
||||
SenderUserID: req.SenderUserID,
|
||||
RecipientUserID: req.RecipientUserID,
|
||||
RandomID: req.RandomID,
|
||||
IdempotencyFingerprint: fingerprint,
|
||||
})
|
||||
if err != nil || found {
|
||||
return replay, err
|
||||
}
|
||||
req.IdempotencyPreflighted = true
|
||||
}
|
||||
}
|
||||
if err := s.ensureCanSend(ctx, req.SenderUserID); err != nil {
|
||||
return domain.SendPrivateTextResult{}, err
|
||||
}
|
||||
|
|
@ -113,6 +136,26 @@ func (s *Service) SendPrivateText(ctx context.Context, userID int64, req domain.
|
|||
return res, err
|
||||
}
|
||||
|
||||
// LookupPrivateSendReplay exposes the immutable receipt to the RPC boundary without executing
|
||||
// send permission checks, business automation or bot responders. Sender identity is still bound
|
||||
// to the authenticated app-service caller.
|
||||
func (s *Service) LookupPrivateSendReplay(ctx context.Context, userID int64, req domain.PrivateSendReplayRequest) (domain.SendPrivateTextResult, bool, error) {
|
||||
if s == nil || s.messages == nil || userID == 0 {
|
||||
return domain.SendPrivateTextResult{}, false, nil
|
||||
}
|
||||
if req.SenderUserID == 0 {
|
||||
req.SenderUserID = userID
|
||||
}
|
||||
if req.SenderUserID != userID || req.RecipientUserID == 0 || req.RandomID == 0 {
|
||||
return domain.SendPrivateTextResult{}, false, fmt.Errorf("private send replay: invalid authenticated scope")
|
||||
}
|
||||
replayStore, ok := s.messages.(store.PrivateSendReplayStore)
|
||||
if !ok {
|
||||
return domain.SendPrivateTextResult{}, false, nil
|
||||
}
|
||||
return replayStore.LookupPrivateSendReplay(ctx, req)
|
||||
}
|
||||
|
||||
func (s *Service) ensureCanSend(ctx context.Context, userID int64) error {
|
||||
if s == nil || s.sendGate == nil || userID == 0 {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -29,6 +29,38 @@ func TestServiceSendPrivateTextHonorsSendPermissionGate(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestServicePrivateReplayPrecedesCurrentSendPermissionGate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
messages := memory.NewMessageStore()
|
||||
allowed := NewService(messages, nil)
|
||||
req := domain.SendPrivateTextRequest{
|
||||
SenderUserID: 1001,
|
||||
RecipientUserID: 1002,
|
||||
RandomID: 91,
|
||||
Message: "committed before restriction",
|
||||
Date: 1_700_000_000,
|
||||
}
|
||||
first, err := allowed.SendPrivateText(ctx, 1001, req)
|
||||
if err != nil {
|
||||
t.Fatalf("first SendPrivateText: %v", err)
|
||||
}
|
||||
|
||||
denied := NewService(messages, nil, WithSendPermissionChecker(denySendChecker{}))
|
||||
req.Date++ // execution time is not part of the immutable send intent.
|
||||
replay, err := denied.SendPrivateText(ctx, 1001, req)
|
||||
if err != nil {
|
||||
t.Fatalf("replay through denied gate: %v", err)
|
||||
}
|
||||
if !replay.Duplicate || replay.SenderMessage.ID != first.SenderMessage.ID {
|
||||
t.Fatalf("replay = %+v, want committed duplicate %d", replay, first.SenderMessage.ID)
|
||||
}
|
||||
|
||||
req.Message = "different intent"
|
||||
if _, err := denied.SendPrivateText(ctx, 1001, req); !errors.Is(err, domain.ErrMessageRandomIDDuplicate) {
|
||||
t.Fatalf("conflicting replay err=%v, want ErrMessageRandomIDDuplicate before send gate", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceForwardPrivateMessagesHonorsSendPermissionGate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := &gateMessageStore{}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue