merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
|
|
@ -72,6 +72,10 @@ func (s *Service) prepareBusinessAutomation(ctx context.Context, req domain.Send
|
|||
if !s.shouldConsiderBusinessAutomation(req) {
|
||||
return businessAutomationContext{}, false
|
||||
}
|
||||
hasAutomation, err := s.business.store.HasBusinessAutomation(ctx, req.RecipientUserID)
|
||||
if err != nil || !hasAutomation {
|
||||
return businessAutomationContext{}, false
|
||||
}
|
||||
out := businessAutomationContext{
|
||||
ownerUserID: req.RecipientUserID,
|
||||
customerUserID: req.SenderUserID,
|
||||
|
|
|
|||
|
|
@ -11,17 +11,22 @@ import (
|
|||
|
||||
// Service 提供消息历史、搜索与已读业务。
|
||||
type Service struct {
|
||||
messages store.MessageStore
|
||||
dialogs store.DialogStore
|
||||
contacts store.ContactStore
|
||||
photos userprojection.ProfilePhotoProvider
|
||||
privacy userprojection.PrivacyEvaluator
|
||||
freezes userprojection.AccountFreezeProvider
|
||||
versions store.ReadModelVersionStore
|
||||
projector *userprojection.Projector
|
||||
botResponder BotResponder
|
||||
sendGate SendPermissionChecker
|
||||
business *businessAutomationConfig
|
||||
messages store.MessageStore
|
||||
dialogs store.DialogStore
|
||||
contacts store.ContactStore
|
||||
photos userprojection.ProfilePhotoProvider
|
||||
privacy userprojection.PrivacyEvaluator
|
||||
freezes userprojection.AccountFreezeProvider
|
||||
versions store.ReadModelVersionStore
|
||||
projector *userprojection.Projector
|
||||
// viewerProjectionComplete is true only when every viewer-scoped user
|
||||
// overlay used by the shared RPC Users service is configured here too.
|
||||
// A partially configured service may still project the dependencies it has,
|
||||
// but RPC must not trust that partial envelope as authoritative.
|
||||
viewerProjectionComplete bool
|
||||
botResponder BotResponder
|
||||
sendGate SendPermissionChecker
|
||||
business *businessAutomationConfig
|
||||
|
||||
privateMediaCountCache *privateMediaCountReadModelCache
|
||||
}
|
||||
|
|
@ -37,7 +42,7 @@ type BotResponder interface {
|
|||
// HandlesBot 报告 botUserID 是否为该 responder 负责的内置 bot。
|
||||
HandlesBot(botUserID int64) bool
|
||||
// OnPrivateMessage 处理一条投递给内置 bot 的消息;msg 为 bot 视角收件 box 行。
|
||||
OnPrivateMessage(ctx context.Context, botUserID int64, msg domain.Message)
|
||||
OnPrivateMessage(ctx context.Context, botUserID int64, msg domain.Message, session domain.ClientSessionMetadata)
|
||||
}
|
||||
|
||||
// Option adjusts optional message service dependencies.
|
||||
|
|
@ -92,9 +97,18 @@ func NewService(messages store.MessageStore, dialogs store.DialogStore, opts ...
|
|||
userprojection.WithPrivacyEvaluator(s.privacy),
|
||||
userprojection.WithAccountFreezeProvider(s.freezes),
|
||||
)
|
||||
s.viewerProjectionComplete = s.contacts != nil && s.photos != nil && s.privacy != nil && s.freezes != nil
|
||||
return s
|
||||
}
|
||||
|
||||
// ProjectsMessageUsersForViewer reports that history/search results returned by
|
||||
// this service have already passed through the viewer-specific user projection
|
||||
// boundary. RPC may reuse that envelope and resolve only nested message refs;
|
||||
// raw stores and test doubles do not implicitly gain this trust marker.
|
||||
func (s *Service) ProjectsMessageUsersForViewer() bool {
|
||||
return s != nil && s.viewerProjectionComplete
|
||||
}
|
||||
|
||||
// SendPrivateText 发送一条私聊文本消息。
|
||||
func (s *Service) SendPrivateText(ctx context.Context, userID int64, req domain.SendPrivateTextRequest) (domain.SendPrivateTextResult, error) {
|
||||
if s == nil || s.messages == nil || userID == 0 {
|
||||
|
|
@ -140,7 +154,7 @@ func (s *Service) SendPrivateText(ctx context.Context, userID int64, req domain.
|
|||
// 兜错,不回传失败。bot 自己发出的消息不触发(SenderUserID 不会是内置 bot
|
||||
// 的对话对象集合里关心的方向——hook 只看收件人)。
|
||||
if err == nil && !res.Duplicate && req.BusinessAutomationKind == "" && s.botResponder != nil && s.botResponder.HandlesBot(req.RecipientUserID) {
|
||||
s.botResponder.OnPrivateMessage(ctx, req.RecipientUserID, res.RecipientMessage)
|
||||
s.botResponder.OnPrivateMessage(ctx, req.RecipientUserID, res.RecipientMessage, req.OriginClientSession)
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
|
@ -349,7 +363,11 @@ func (s *Service) SearchPrivateMedia(ctx context.Context, userID, peerID int64,
|
|||
if s == nil || s.messages == nil || userID == 0 || peerID == 0 {
|
||||
return domain.MessageList{}, nil
|
||||
}
|
||||
return s.messages.SearchPrivateMedia(ctx, userID, peerID, req)
|
||||
list, err := s.messages.SearchPrivateMedia(ctx, userID, peerID, req)
|
||||
if err != nil {
|
||||
return domain.MessageList{}, err
|
||||
}
|
||||
return s.projectMessageUsers(ctx, userID, list)
|
||||
}
|
||||
|
||||
// CountPrivateMediaCategories 返回某私聊会话按基础媒体类别聚合的精确计数。
|
||||
|
|
|
|||
|
|
@ -104,6 +104,9 @@ func TestServiceProjectsMessageUsersForViewerContacts(t *testing.T) {
|
|||
friendID: {PhotoID: 9101, DCID: 2, Stripped: []byte{5, 6}},
|
||||
strangerID: {PhotoID: 9102, DCID: 4},
|
||||
}))
|
||||
if svc.ProjectsMessageUsersForViewer() {
|
||||
t.Fatal("partially configured message projector must not claim a complete viewer envelope")
|
||||
}
|
||||
|
||||
list, err := svc.GetHistory(ctx, ownerID, domain.MessageFilter{Limit: 10})
|
||||
if err != nil {
|
||||
|
|
@ -127,6 +130,62 @@ func TestServiceProjectsMessageUsersForViewerContacts(t *testing.T) {
|
|||
if self.Phone != "15550000001" {
|
||||
t.Fatalf("self phone = %q, want preserved", self.Phone)
|
||||
}
|
||||
|
||||
media, err := svc.SearchPrivateMedia(ctx, ownerID, friendID, domain.MediaSearchRequest{Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("SearchPrivateMedia: %v", err)
|
||||
}
|
||||
mediaFriend := findUser(t, media.Users, friendID)
|
||||
if !mediaFriend.Contact || mediaFriend.FirstName != "Remark" || mediaFriend.Phone != "15550000002" || mediaFriend.PhotoID != 9101 {
|
||||
t.Fatalf("shared-media friend projection = %+v, want the same viewer projection as history", mediaFriend)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceMarksOnlyFullyConfiguredViewerProjectionComplete(t *testing.T) {
|
||||
store := projectionMessageStore{}
|
||||
svc := NewService(store, nil,
|
||||
WithContactStore(memory.NewContactStore()),
|
||||
WithPhotoProvider(messageProfilePhotos{}),
|
||||
WithPrivacyEvaluator(messageProjectionPrivacy{}),
|
||||
WithAccountFreezeProvider(messageProjectionFreezes{}),
|
||||
)
|
||||
if !svc.ProjectsMessageUsersForViewer() {
|
||||
t.Fatal("fully configured message projector must advertise a complete viewer envelope")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendPrivateTextWithoutBusinessAutomationSkipsDialogAndContactReads(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
const ownerID int64 = 2001
|
||||
const customerID int64 = 2002
|
||||
|
||||
dialogs := memory.NewDialogStore()
|
||||
messages := memory.NewMessageStore(dialogs)
|
||||
business := &countingBusinessAutomationStore{BusinessAutomationStore: memory.NewPasswordStore()}
|
||||
countingDialogs := &countingBusinessDialogStore{DialogStore: dialogs}
|
||||
countingContacts := &countingBusinessContactStore{ContactStore: memory.NewContactStore()}
|
||||
svc := NewService(
|
||||
messages,
|
||||
countingDialogs,
|
||||
WithBusinessAutomation(business),
|
||||
WithContactStore(countingContacts),
|
||||
)
|
||||
|
||||
if _, err := svc.SendPrivateText(ctx, customerID, domain.SendPrivateTextRequest{
|
||||
SenderUserID: customerID,
|
||||
RecipientUserID: ownerID,
|
||||
RandomID: 9001,
|
||||
Message: "ordinary message",
|
||||
Date: 1_700_000_000,
|
||||
}); err != nil {
|
||||
t.Fatalf("SendPrivateText: %v", err)
|
||||
}
|
||||
if business.hasCalls != 1 {
|
||||
t.Fatalf("HasBusinessAutomation calls = %d, want one lightweight gate", business.hasCalls)
|
||||
}
|
||||
if countingDialogs.listByPeersCalls != 0 || countingContacts.getCalls != 0 {
|
||||
t.Fatalf("business detail reads dialogs/contacts = %d/%d, want 0/0 without automation", countingDialogs.listByPeersCalls, countingContacts.getCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBusinessAutomationGreetingSendsQuickReplyWithoutLoop(t *testing.T) {
|
||||
|
|
@ -567,6 +626,36 @@ type staticBusinessAutomationProvider struct {
|
|||
message string
|
||||
}
|
||||
|
||||
type countingBusinessAutomationStore struct {
|
||||
store.BusinessAutomationStore
|
||||
hasCalls int
|
||||
}
|
||||
|
||||
func (s *countingBusinessAutomationStore) HasBusinessAutomation(ctx context.Context, userID int64) (bool, error) {
|
||||
s.hasCalls++
|
||||
return s.BusinessAutomationStore.HasBusinessAutomation(ctx, userID)
|
||||
}
|
||||
|
||||
type countingBusinessDialogStore struct {
|
||||
store.DialogStore
|
||||
listByPeersCalls int
|
||||
}
|
||||
|
||||
func (s *countingBusinessDialogStore) ListByPeers(ctx context.Context, userID int64, peers []domain.Peer) (domain.DialogList, error) {
|
||||
s.listByPeersCalls++
|
||||
return s.DialogStore.ListByPeers(ctx, userID, peers)
|
||||
}
|
||||
|
||||
type countingBusinessContactStore struct {
|
||||
store.ContactStore
|
||||
getCalls int
|
||||
}
|
||||
|
||||
func (s *countingBusinessContactStore) Get(ctx context.Context, userID, contactUserID int64) (domain.Contact, bool, error) {
|
||||
s.getCalls++
|
||||
return s.ContactStore.Get(ctx, userID, contactUserID)
|
||||
}
|
||||
|
||||
func (p staticBusinessAutomationProvider) BusinessAutomationReplies(context.Context, BusinessAutomationReplyInput) ([]domain.QuickReplyMessage, error) {
|
||||
return []domain.QuickReplyMessage{{ID: 1, Message: p.message}}, nil
|
||||
}
|
||||
|
|
@ -751,9 +840,21 @@ func (s projectionMessageStore) ListByUser(context.Context, int64, domain.Messag
|
|||
}
|
||||
|
||||
func (s projectionMessageStore) SearchPrivateMedia(context.Context, int64, int64, domain.MediaSearchRequest) (domain.MessageList, error) {
|
||||
return domain.MessageList{}, nil
|
||||
return s.list, nil
|
||||
}
|
||||
|
||||
func (s projectionMessageStore) CountPrivateMediaCategories(context.Context, int64, int64) (domain.MediaCategoryCounts, error) {
|
||||
return domain.MediaCategoryCounts{}, nil
|
||||
}
|
||||
|
||||
type messageProjectionPrivacy struct{}
|
||||
|
||||
func (messageProjectionPrivacy) CanSee(context.Context, int64, int64, domain.PrivacyKey) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type messageProjectionFreezes struct{}
|
||||
|
||||
func (messageProjectionFreezes) AccountFreezes(context.Context, []int64) (map[int64]domain.AccountFreeze, error) {
|
||||
return map[int64]domain.AccountFreeze{}, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue