merged with fixes
This commit is contained in:
parent
a9e758b712
commit
2f1818d656
176 changed files with 9000 additions and 907 deletions
|
|
@ -435,6 +435,9 @@ func mergeAuthorizationClientInfo(a *domain.Authorization, info domain.AuthKeyCl
|
|||
if info.AppVersion != "" {
|
||||
a.AppVersion = info.AppVersion
|
||||
}
|
||||
if info.IP != "" {
|
||||
a.IP = info.IP
|
||||
}
|
||||
a.ActiveAt = time.Now()
|
||||
}
|
||||
|
||||
|
|
|
|||
43
internal/store/memory/bot_branding_test.go
Normal file
43
internal/store/memory/bot_branding_test.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/branding"
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func TestBuiltInBotSeedsUseConfiguredProductName(t *testing.T) {
|
||||
previous := branding.Current()
|
||||
t.Cleanup(func() {
|
||||
if err := branding.Configure(previous); err != nil {
|
||||
t.Fatalf("restore branding: %v", err)
|
||||
}
|
||||
})
|
||||
cfg := previous
|
||||
cfg.ProductName = "Example Chat"
|
||||
if err := branding.Configure(cfg); err != nil {
|
||||
t.Fatalf("configure branding: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
users := NewUserStore()
|
||||
bots := NewBotStore(users)
|
||||
for _, test := range []struct {
|
||||
id int64
|
||||
want string
|
||||
}{
|
||||
{id: domain.ChatBotUserID, want: domain.ChatBotDescription()},
|
||||
{id: domain.StickersBotUserID, want: domain.StickersBotDescription()},
|
||||
} {
|
||||
user, found, err := users.ByID(ctx, test.id)
|
||||
if err != nil || !found || user.About != test.want {
|
||||
t.Fatalf("bot user %d = %+v found=%v err=%v, want about %q", test.id, user, found, err, test.want)
|
||||
}
|
||||
profile, found, err := bots.GetBot(ctx, test.id)
|
||||
if err != nil || !found || profile.Description != test.want {
|
||||
t.Fatalf("bot profile %d = %+v found=%v err=%v, want description %q", test.id, profile, found, err, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
93
internal/store/memory/media_pagination_test.go
Normal file
93
internal/store/memory/media_pagination_test.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"telesrv/internal/domain"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMediaPaginationBoundaries(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
messages := NewMessageStore()
|
||||
channels := NewChannelStore()
|
||||
owner, other := int64(71), int64(72)
|
||||
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{CreatorUserID: owner, Title: "media boundaries", Megagroup: true, MemberUserIDs: []int64{other}, Date: 1700000000})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
channel := created.Channel.ID
|
||||
|
||||
var privateIDs, channelIDs []int
|
||||
for i := 1; i <= 12; i++ {
|
||||
var media *domain.MessageMedia
|
||||
if i%2 == 1 {
|
||||
media = &domain.MessageMedia{Kind: domain.MessageMediaKindPhoto, Photo: &domain.Photo{ID: int64(i), AccessHash: 99}}
|
||||
}
|
||||
a, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{SenderUserID: owner, RecipientUserID: other, RandomID: int64(i), Message: "media boundary", Media: media, Date: 1700000000 + i})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{UserID: owner, ChannelID: channel, RandomID: int64(i), Message: "media boundary", Media: media, Date: 1700000000 + i})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if media != nil {
|
||||
privateIDs = append([]int{a.SenderMessage.ID}, privateIDs...)
|
||||
channelIDs = append([]int{c.Message.ID}, channelIDs...)
|
||||
}
|
||||
}
|
||||
for _, side := range []string{"private", "channel"} {
|
||||
ids := privateIDs
|
||||
if side == "channel" {
|
||||
ids = channelIDs
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
f domain.MediaSearchRequest
|
||||
want []int
|
||||
count int
|
||||
}{
|
||||
{"all", domain.MediaSearchRequest{Limit: 100}, ids, 6},
|
||||
{"strict-range", domain.MediaSearchRequest{Limit: 100, MinID: ids[4], MaxID: ids[1]}, ids[2:4], 2},
|
||||
{"strict-count", domain.MediaSearchRequest{Limit: 0, MinID: ids[4], MaxID: ids[1], OffsetID: ids[2], AddOffset: -2}, nil, 2},
|
||||
{"around-existing", domain.MediaSearchRequest{Limit: 4, OffsetID: ids[2], AddOffset: -2}, ids[1:5], 6},
|
||||
{"after-missing", domain.MediaSearchRequest{Limit: 2, OffsetID: ids[4] + 1, AddOffset: -2}, ids[2:4], 6},
|
||||
{"forward-gap", domain.MediaSearchRequest{Limit: 2, OffsetID: ids[4], AddOffset: -4}, ids[1:3], 6},
|
||||
{"empty-forward", domain.MediaSearchRequest{Limit: 2, OffsetID: ids[0] + 1, AddOffset: -2}, nil, 6},
|
||||
{"around-top", domain.MediaSearchRequest{Limit: 4, OffsetID: ids[0] + 1, AddOffset: -2}, ids[:2], 6},
|
||||
{"around-zero", domain.MediaSearchRequest{Limit: 4, AddOffset: -2}, ids[:2], 6},
|
||||
{"empty-backward", domain.MediaSearchRequest{Limit: 2, AddOffset: 100}, nil, 6},
|
||||
} {
|
||||
t.Run(side+"/"+tc.name, func(t *testing.T) {
|
||||
f := tc.f
|
||||
f.Categories = []domain.MediaCategory{domain.MediaCategoryPhoto, domain.MediaCategoryPhoto}
|
||||
f.Query = "boundary"
|
||||
var got []int
|
||||
var count int
|
||||
if side == "private" {
|
||||
r, err := messages.SearchPrivateMedia(ctx, owner, other, f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
count = r.Count
|
||||
for _, m := range r.Messages {
|
||||
got = append(got, m.ID)
|
||||
}
|
||||
} else {
|
||||
r, err := channels.SearchChannelMedia(ctx, owner, channel, f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
count = r.Count
|
||||
for _, m := range r.Messages {
|
||||
got = append(got, m.ID)
|
||||
}
|
||||
}
|
||||
if count != tc.count || !reflect.DeepEqual(append([]int{}, got...), append([]int{}, tc.want...)) {
|
||||
t.Fatalf("ids=%v count=%d want %v/%d", got, count, tc.want, tc.count)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -67,30 +67,16 @@ func pageMediaIDs(ids []int, req domain.MediaSearchRequest) ([]int, int) {
|
|||
sort.Sort(sort.Reverse(sort.IntSlice(ids)))
|
||||
inRange := make([]int, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if req.MaxID != 0 && id > req.MaxID {
|
||||
if req.MaxID != 0 && id >= req.MaxID {
|
||||
continue
|
||||
}
|
||||
if req.MinID != 0 && id < req.MinID {
|
||||
if req.MinID != 0 && id <= req.MinID {
|
||||
continue
|
||||
}
|
||||
inRange = append(inRange, id)
|
||||
}
|
||||
count := len(inRange)
|
||||
page := make([]int, 0, len(inRange))
|
||||
for _, id := range inRange {
|
||||
if req.OffsetID != 0 && id >= req.OffsetID {
|
||||
continue
|
||||
}
|
||||
page = append(page, id)
|
||||
}
|
||||
off := req.AddOffset
|
||||
if off < 0 {
|
||||
off = 0
|
||||
}
|
||||
if off > len(page) {
|
||||
off = len(page)
|
||||
}
|
||||
page = page[off:]
|
||||
|
||||
limit := req.Limit
|
||||
if limit == 0 {
|
||||
return nil, count
|
||||
|
|
@ -98,9 +84,15 @@ func pageMediaIDs(ids []int, req domain.MediaSearchRequest) ([]int, int) {
|
|||
if limit < 0 || limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
if len(page) > limit {
|
||||
page = page[:limit]
|
||||
pivot := 0
|
||||
if req.OffsetID > 0 {
|
||||
pivot = sort.Search(len(inRange), func(i int) bool { return inRange[i] < req.OffsetID })
|
||||
}
|
||||
start := pivot + domain.ClampMessageHistoryAddOffset(req.AddOffset)
|
||||
end := start + limit
|
||||
start = min(max(start, 0), len(inRange))
|
||||
end = min(max(end, start), len(inRange))
|
||||
page := inRange[start:end]
|
||||
return page, count
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -186,12 +186,7 @@ func richMessagesEqual(a, b *domain.MessageRichMessage) bool {
|
|||
}
|
||||
|
||||
func cloneMessageReply(reply *domain.MessageReply) *domain.MessageReply {
|
||||
if reply == nil {
|
||||
return nil
|
||||
}
|
||||
clone := *reply
|
||||
clone.QuoteEntities = append([]domain.MessageEntity(nil), reply.QuoteEntities...)
|
||||
return &clone
|
||||
return domain.CloneMessageReply(reply)
|
||||
}
|
||||
|
||||
func cloneMessageForward(forward *domain.MessageForward) *domain.MessageForward {
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ func (s *MessageStore) memoryHistoryClearAnchorLocked(userID int64, peer domain.
|
|||
func filterMessageList(messages []domain.Message, filter domain.MessageFilter) domain.MessageList {
|
||||
filter.AddOffset = domain.ClampMessageHistoryAddOffset(filter.AddOffset)
|
||||
sort.SliceStable(messages, func(i, j int) bool {
|
||||
return messageLess(messages[i], messages[j])
|
||||
return messages[i].ID > messages[j].ID
|
||||
})
|
||||
|
||||
query := strings.ToLower(filter.Query)
|
||||
|
|
@ -308,6 +308,9 @@ func filterMessageList(messages []domain.Message, filter domain.MessageFilter) d
|
|||
if filter.HasPeer && msg.Peer != filter.Peer {
|
||||
continue
|
||||
}
|
||||
if filter.SenderUserID != 0 && msg.From != (domain.Peer{Type: domain.PeerTypeUser, ID: filter.SenderUserID}) {
|
||||
continue
|
||||
}
|
||||
if filter.RestrictPeerIDs {
|
||||
if msg.Peer.Type != domain.PeerTypeUser {
|
||||
continue
|
||||
|
|
@ -346,6 +349,9 @@ func filterMessageList(messages []domain.Message, filter domain.MessageFilter) d
|
|||
base = append(base, msg)
|
||||
}
|
||||
|
||||
if filter.CountOnly {
|
||||
return domain.MessageList{Count: len(base)}
|
||||
}
|
||||
limit := filter.Limit
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
|
|
@ -384,7 +390,7 @@ func pageMessageHistory(base []domain.Message, filter domain.MessageFilter, limi
|
|||
}
|
||||
switch messageHistoryLoadType(filter.AddOffset, limit) {
|
||||
case messageHistoryLoadForward:
|
||||
return cloneMessages(forwardMessageHistory(base, filter, limit))
|
||||
return cloneMessages(forwardMessageHistory(base, filter, limit, -filter.AddOffset-limit))
|
||||
case messageHistoryLoadAround:
|
||||
forwardLimit := -filter.AddOffset
|
||||
if forwardLimit > limit {
|
||||
|
|
@ -395,10 +401,10 @@ func pageMessageHistory(base []domain.Message, filter domain.MessageFilter, limi
|
|||
backwardLimit = 0
|
||||
}
|
||||
page := make([]domain.Message, 0, limit)
|
||||
page = append(page, forwardMessageHistory(base, filter, forwardLimit)...)
|
||||
page = append(page, backwardMessageHistory(base, filter, backwardLimit, true)...)
|
||||
page = append(page, forwardMessageHistory(base, filter, forwardLimit, 0)...)
|
||||
page = append(page, backwardMessageHistory(base, filter, backwardLimit)...)
|
||||
sort.SliceStable(page, func(i, j int) bool {
|
||||
return messageLess(page[i], page[j])
|
||||
return page[i].ID > page[j].ID
|
||||
})
|
||||
return cloneMessages(page)
|
||||
default:
|
||||
|
|
@ -406,7 +412,7 @@ func pageMessageHistory(base []domain.Message, filter domain.MessageFilter, limi
|
|||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
candidates := backwardMessageHistory(base, filter, limit+start, false)
|
||||
candidates := backwardMessageHistory(base, filter, limit+start)
|
||||
if start >= len(candidates) {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -432,13 +438,13 @@ func messageHistoryLoadType(addOffset, limit int) messageHistoryLoad {
|
|||
return messageHistoryLoadForward
|
||||
}
|
||||
|
||||
func backwardMessageHistory(base []domain.Message, filter domain.MessageFilter, limit int, includeOffset bool) []domain.Message {
|
||||
func backwardMessageHistory(base []domain.Message, filter domain.MessageFilter, limit int) []domain.Message {
|
||||
if limit <= 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]domain.Message, 0, limit)
|
||||
for _, msg := range base {
|
||||
if !messageBeforeHistoryOffset(msg, filter, includeOffset) {
|
||||
if !messageBeforeHistoryOffset(msg, filter) {
|
||||
continue
|
||||
}
|
||||
out = append(out, msg)
|
||||
|
|
@ -449,7 +455,7 @@ func backwardMessageHistory(base []domain.Message, filter domain.MessageFilter,
|
|||
return out
|
||||
}
|
||||
|
||||
func forwardMessageHistory(base []domain.Message, filter domain.MessageFilter, limit int) []domain.Message {
|
||||
func forwardMessageHistory(base []domain.Message, filter domain.MessageFilter, limit, skip int) []domain.Message {
|
||||
if limit <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -459,30 +465,28 @@ func forwardMessageHistory(base []domain.Message, filter domain.MessageFilter, l
|
|||
if !messageAfterHistoryOffset(msg, filter) {
|
||||
continue
|
||||
}
|
||||
if skip > 0 {
|
||||
skip--
|
||||
continue
|
||||
}
|
||||
out = append(out, msg)
|
||||
if len(out) == limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
return messageLess(out[i], out[j])
|
||||
return out[i].ID > out[j].ID
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func messageBeforeHistoryOffset(msg domain.Message, filter domain.MessageFilter, includeOffset bool) bool {
|
||||
func messageBeforeHistoryOffset(msg domain.Message, filter domain.MessageFilter) bool {
|
||||
if filter.OffsetDate > 0 {
|
||||
if includeOffset {
|
||||
return msg.Date <= filter.OffsetDate
|
||||
}
|
||||
return msg.Date < filter.OffsetDate
|
||||
}
|
||||
if filter.OffsetID <= 0 {
|
||||
return true
|
||||
}
|
||||
if includeOffset {
|
||||
return msg.ID <= filter.OffsetID
|
||||
}
|
||||
return msg.ID < filter.OffsetID
|
||||
}
|
||||
|
||||
|
|
@ -493,7 +497,7 @@ func messageAfterHistoryOffset(msg domain.Message, filter domain.MessageFilter)
|
|||
if filter.OffsetID <= 0 {
|
||||
return false
|
||||
}
|
||||
return msg.ID > filter.OffsetID
|
||||
return msg.ID >= filter.OffsetID
|
||||
}
|
||||
|
||||
func messageLess(a, b domain.Message) bool {
|
||||
|
|
|
|||
51
internal/store/memory/message_search_test.go
Normal file
51
internal/store/memory/message_search_test.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"telesrv/internal/domain"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMessageSearchSenderAndCountFilters(t *testing.T) {
|
||||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: 22}
|
||||
var messages []domain.Message
|
||||
for i := 1; i <= 6; i++ {
|
||||
sender := int64(11)
|
||||
if i%2 == 0 {
|
||||
sender = 22
|
||||
}
|
||||
messages = append(messages, domain.Message{ID: i, OwnerUserID: 11, Peer: peer, From: domain.Peer{Type: domain.PeerTypeUser, ID: sender}, Body: "count needle", Date: 100 + i, Pinned: i == 3})
|
||||
}
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
f domain.MessageFilter
|
||||
ids []int
|
||||
count int
|
||||
}{
|
||||
{"sender", domain.MessageFilter{SenderUserID: 11, Limit: 10}, []int{5, 3, 1}, 3},
|
||||
{"sender-around", domain.MessageFilter{SenderUserID: 11, OffsetID: 3, AddOffset: -1, Limit: 3}, []int{3, 1}, 3},
|
||||
{"count-ignores-page", domain.MessageFilter{SenderUserID: 11, CountOnly: true, OffsetID: 1, AddOffset: 100}, nil, 3},
|
||||
{"count-intersection", domain.MessageFilter{SenderUserID: 11, CountOnly: true, MinDate: 101, MaxDate: 106, MinID: 1, MaxID: 5}, nil, 1},
|
||||
{"count-pinned", domain.MessageFilter{SenderUserID: 11, CountOnly: true, PinnedOnly: true}, nil, 1},
|
||||
{"count-pinned-other", domain.MessageFilter{SenderUserID: 22, CountOnly: true, PinnedOnly: true}, nil, 0},
|
||||
{"count-peer-isolation", domain.MessageFilter{CountOnly: true, RestrictPeerIDs: true, PeerIDs: []int64{33}}, nil, 0},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := tt.f
|
||||
f.HasPeer = true
|
||||
f.Peer = peer
|
||||
f.Query = "needle"
|
||||
got := filterMessageList(cloneMessages(messages), f)
|
||||
var ids []int
|
||||
for _, m := range got.Messages {
|
||||
ids = append(ids, m.ID)
|
||||
}
|
||||
if !reflect.DeepEqual(ids, tt.ids) || got.Count != tt.count {
|
||||
t.Fatalf("ids=%v count=%d want %v/%d", ids, got.Count, tt.ids, tt.count)
|
||||
}
|
||||
if f.CountOnly && len(got.Users) > 0 {
|
||||
t.Fatal("count projected users")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -52,8 +52,26 @@ func (s *MessageStore) SendPrivateText(_ context.Context, req domain.SendPrivate
|
|||
if err != nil {
|
||||
return domain.SendPrivateTextResult{}, err
|
||||
}
|
||||
// Match TogglePrivateNoForwards lock order; only external private-source
|
||||
// replies read pair protection while holding the message mutation lock.
|
||||
if r := req.ReplyTo; r != nil && r.Peer.Type == domain.PeerTypeUser && r.Peer.ID > 0 && r.Peer.ID != req.RecipientUserID {
|
||||
s.noForwardsMu.Lock()
|
||||
defer s.noForwardsMu.Unlock()
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.sendPrivateTextLocked(req, fingerprint, nil)
|
||||
}
|
||||
|
||||
// A prefix stages related state events before the message. apply is infallible
|
||||
// and only runs after all validation and receipt encoding have succeeded.
|
||||
type memoryPrivateSendPrefix struct {
|
||||
events []domain.UpdateEvent
|
||||
apply func([]domain.UpdateEvent)
|
||||
}
|
||||
|
||||
// Caller holds s.mu; this helper owns the dialog/event locks through commit.
|
||||
func (s *MessageStore) sendPrivateTextLocked(req domain.SendPrivateTextRequest, fingerprint []byte, prefix *memoryPrivateSendPrefix) (domain.SendPrivateTextResult, error) {
|
||||
if replay, found, err := s.lookupPrivateSendReplayLocked(domain.PrivateSendReplayRequest{
|
||||
SenderUserID: req.SenderUserID,
|
||||
RecipientUserID: req.RecipientUserID,
|
||||
|
|
@ -69,10 +87,37 @@ func (s *MessageStore) SendPrivateText(_ context.Context, req domain.SendPrivate
|
|||
if err != nil {
|
||||
return domain.SendPrivateTextResult{}, err
|
||||
}
|
||||
events := s.updateEvents
|
||||
if events == nil {
|
||||
return domain.SendPrivateTextResult{}, store.ErrDeliveryOutboxRequired
|
||||
}
|
||||
events.mu.Lock()
|
||||
defer events.mu.Unlock()
|
||||
// Stage counters as well as the message: failed encoding cannot consume PTS.
|
||||
allocatedPTS := make(map[int64]int)
|
||||
nextPTS := func(userID int64) int {
|
||||
current, ok := allocatedPTS[userID]
|
||||
if !ok {
|
||||
current = s.nextPts[userID]
|
||||
for _, event := range events.events[userID] {
|
||||
if event.Pts > current {
|
||||
current = event.Pts
|
||||
}
|
||||
}
|
||||
}
|
||||
allocatedPTS[userID] = current + 1
|
||||
return current + 1
|
||||
}
|
||||
var prefixEvents []domain.UpdateEvent
|
||||
if prefix != nil {
|
||||
for _, event := range prefix.events {
|
||||
event.Pts = nextPTS(event.UserID)
|
||||
prefixEvents = append(prefixEvents, event)
|
||||
}
|
||||
}
|
||||
uid := s.nextUID
|
||||
s.nextUID++
|
||||
sender := domain.Message{
|
||||
ID: s.nextBoxIDLocked(req.SenderUserID),
|
||||
ID: s.nextBox[req.SenderUserID] + 1,
|
||||
UID: uid,
|
||||
RandomID: req.RandomID,
|
||||
OwnerUserID: req.SenderUserID,
|
||||
|
|
@ -92,7 +137,7 @@ func (s *MessageStore) SendPrivateText(_ context.Context, req domain.SendPrivate
|
|||
RichMessage: cloneRichMessage(req.RichMessage),
|
||||
ReplyTo: cloneMessageReply(senderReply),
|
||||
Forward: cloneMessageForward(req.Forward),
|
||||
Pts: s.nextPtsLocked(req.SenderUserID),
|
||||
Pts: nextPTS(req.SenderUserID),
|
||||
// voice/round 在发送者副本上同样保持"未听",由对端内容已读清除。
|
||||
MediaUnread: req.Media.HasUnreadPayload() && req.SenderUserID != req.RecipientUserID,
|
||||
}
|
||||
|
|
@ -105,7 +150,7 @@ func (s *MessageStore) SendPrivateText(_ context.Context, req domain.SendPrivate
|
|||
}
|
||||
if req.SenderUserID != req.RecipientUserID && !req.RecipientBlocked {
|
||||
recipient = sender
|
||||
recipient.ID = s.nextBoxIDLocked(req.RecipientUserID)
|
||||
recipient.ID = s.nextBox[req.RecipientUserID] + 1
|
||||
recipient.OwnerUserID = req.RecipientUserID
|
||||
recipient.Peer = domain.Peer{Type: domain.PeerTypeUser, ID: req.SenderUserID}
|
||||
recipient.Out = false
|
||||
|
|
@ -115,7 +160,7 @@ func (s *MessageStore) SendPrivateText(_ context.Context, req domain.SendPrivate
|
|||
// 让双盒各持独立快照(与 postgres 每盒独立 decode 对齐,I3/I2)。
|
||||
recipient.ReplyMarkup = cloneReplyMarkup(sender.ReplyMarkup)
|
||||
recipient.RichMessage = cloneRichMessage(sender.RichMessage)
|
||||
recipient.Pts = s.nextPtsLocked(req.RecipientUserID)
|
||||
recipient.Pts = nextPTS(req.RecipientUserID)
|
||||
recipient.MediaUnread = req.Media.HasUnreadPayload()
|
||||
}
|
||||
var senderSnapshot []byte
|
||||
|
|
@ -125,6 +170,25 @@ func (s *MessageStore) SendPrivateText(_ context.Context, req domain.SendPrivate
|
|||
return domain.SendPrivateTextResult{}, err
|
||||
}
|
||||
}
|
||||
// No fallible work follows: publish the prefix and message under all locks.
|
||||
if prefix != nil {
|
||||
prefix.apply(prefixEvents)
|
||||
}
|
||||
for _, event := range prefixEvents {
|
||||
auth, session := [8]byte{}, int64(0)
|
||||
if event.UserID == req.SenderUserID {
|
||||
auth, session = req.OriginAuthKeyID, req.OriginSessionID
|
||||
}
|
||||
appendMemorySendEventLocked(events, event.UserID, event, auth, session)
|
||||
}
|
||||
s.nextUID++
|
||||
s.nextBox[req.SenderUserID] = sender.ID
|
||||
if recipient.ID != 0 {
|
||||
s.nextBox[recipient.OwnerUserID] = recipient.ID
|
||||
}
|
||||
for userID, pts := range allocatedPTS {
|
||||
s.nextPts[userID] = pts
|
||||
}
|
||||
s.m[req.SenderUserID] = append(s.m[req.SenderUserID], sender)
|
||||
if req.SenderUserID != req.RecipientUserID && !req.RecipientBlocked {
|
||||
s.m[req.RecipientUserID] = append(s.m[req.RecipientUserID], recipient)
|
||||
|
|
@ -144,6 +208,22 @@ func (s *MessageStore) SendPrivateText(_ context.Context, req domain.SendPrivate
|
|||
s.upsertMemoryDialogsLocked(sender, sender)
|
||||
}
|
||||
}
|
||||
originUserID := req.OriginUserID
|
||||
if originUserID == 0 {
|
||||
originUserID = req.SenderUserID
|
||||
}
|
||||
senderExcludeAuthKeyID, senderExcludeSessionID := [8]byte{}, int64(0)
|
||||
if originUserID == req.SenderUserID {
|
||||
senderExcludeAuthKeyID, senderExcludeSessionID = req.OriginAuthKeyID, req.OriginSessionID
|
||||
}
|
||||
appendMemorySendEventLocked(events, req.SenderUserID, newMessageEvent(sender), senderExcludeAuthKeyID, senderExcludeSessionID)
|
||||
if recipient.ID != 0 && recipient.OwnerUserID != sender.OwnerUserID {
|
||||
recipientExcludeAuthKeyID, recipientExcludeSessionID := [8]byte{}, int64(0)
|
||||
if originUserID == req.RecipientUserID {
|
||||
recipientExcludeAuthKeyID, recipientExcludeSessionID = req.OriginAuthKeyID, req.OriginSessionID
|
||||
}
|
||||
appendMemorySendEventLocked(events, req.RecipientUserID, newMessageEvent(recipient), recipientExcludeAuthKeyID, recipientExcludeSessionID)
|
||||
}
|
||||
return domain.SendPrivateTextResult{
|
||||
SenderMessage: cloneMessage(sender),
|
||||
RecipientMessage: cloneMessage(recipient),
|
||||
|
|
@ -152,6 +232,25 @@ func (s *MessageStore) SendPrivateText(_ context.Context, req domain.SendPrivate
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *MessageStore) nextPtsWithEventsLocked(events *UpdateEventStore, userID int64) int {
|
||||
current := s.nextPts[userID]
|
||||
for _, event := range events.events[userID] {
|
||||
if event.Pts > current {
|
||||
current = event.Pts
|
||||
}
|
||||
}
|
||||
current++
|
||||
s.nextPts[userID] = current
|
||||
return current
|
||||
}
|
||||
|
||||
func appendMemorySendEventLocked(events *UpdateEventStore, userID int64, event domain.UpdateEvent, excludeAuthKeyID [8]byte, excludeSessionID int64) {
|
||||
event = events.appendLocked(userID, event, false)
|
||||
events.dispatches[userID] = append(events.dispatches[userID], memoryUpdateDispatch{
|
||||
Pts: event.Pts, ExcludeAuthKeyID: excludeAuthKeyID, ExcludeSessionID: excludeSessionID,
|
||||
})
|
||||
}
|
||||
|
||||
// LookupPrivateSendReplay returns an existing immutable/current replay receipt without running
|
||||
// any send permission, reply resolution or allocation path.
|
||||
func (s *MessageStore) LookupPrivateSendReplay(_ context.Context, req domain.PrivateSendReplayRequest) (domain.SendPrivateTextResult, bool, error) {
|
||||
|
|
@ -246,7 +345,7 @@ func (s *MessageStore) resolveMemoryReplyLocked(req domain.SendPrivateTextReques
|
|||
if peer.ID == 0 {
|
||||
peer = domain.Peer{Type: domain.PeerTypeUser, ID: req.RecipientUserID}
|
||||
}
|
||||
if peer.Type != domain.PeerTypeUser || peer.ID != req.RecipientUserID {
|
||||
if peer.Type != domain.PeerTypeUser || req.ReplyTo.External != nil {
|
||||
return nil, nil, domain.ErrReplyMessageIDInvalid
|
||||
}
|
||||
var target domain.Message
|
||||
|
|
@ -262,6 +361,29 @@ func (s *MessageStore) resolveMemoryReplyLocked(req domain.SendPrivateTextReques
|
|||
senderReply := cloneMessageReply(req.ReplyTo)
|
||||
senderReply.MessageID = target.ID
|
||||
senderReply.Peer = peer
|
||||
if peer.ID != req.RecipientUserID {
|
||||
protected := target.NoForwards || (target.Media != nil && target.Media.TTLSeconds > 0)
|
||||
if pair, ok := noForwardsPair(req.SenderUserID, peer.ID); ok {
|
||||
protected = protected || s.privateNoForwards[pair].Enabled()
|
||||
}
|
||||
if protected {
|
||||
return nil, nil, domain.ErrChatForwardsRestricted
|
||||
}
|
||||
if err := domain.ValidateExternalReplyQuote(req.ReplyTo, target.Body); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var err error
|
||||
senderReply.External, err = domain.NewMessageReplyExternal(target)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
recipientReply := cloneMessageReply(senderReply)
|
||||
if req.SenderUserID != req.RecipientUserID {
|
||||
recipientReply.MessageID = 0
|
||||
recipientReply.TopMessageID = 0
|
||||
}
|
||||
return senderReply, recipientReply, nil
|
||||
}
|
||||
if req.SenderUserID == req.RecipientUserID {
|
||||
return senderReply, cloneMessageReply(senderReply), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ type MessageStore struct {
|
|||
loginCodeDeliveries map[[32]byte]loginCodeDeliveryRecord
|
||||
albumGroups map[albumGroupKey]albumGroupRecord
|
||||
dialogs *DialogStore
|
||||
updateEvents *UpdateEventStore
|
||||
// polls 是共享 poll 权威(投票校验与读路径 enrichment);nil 时 poll 链路按未接入处理。
|
||||
polls *PollStore
|
||||
// savedPins 是收藏夹子会话置顶顺序(下标即 pinned_order,越小越前)。
|
||||
|
|
@ -37,6 +38,10 @@ func (s *MessageStore) AttachPollStore(polls *PollStore) {
|
|||
s.polls = polls
|
||||
}
|
||||
|
||||
func (s *MessageStore) AttachUpdateEventStore(events *UpdateEventStore) {
|
||||
s.updateEvents = events
|
||||
}
|
||||
|
||||
type readOutboxDateKey struct {
|
||||
ownerUserID int64
|
||||
peerID int64
|
||||
|
|
@ -60,6 +65,7 @@ func NewMessageStore(dialogs ...*DialogStore) *MessageStore {
|
|||
savedPins: make(map[int64][]domain.Peer),
|
||||
privateNoForwards: make(map[privateNoForwardsPair]domain.PrivateNoForwardsState),
|
||||
privateNoForwardsRequests: make(map[int64]memoryNoForwardsRequest),
|
||||
updateEvents: NewUpdateEventStore(),
|
||||
}
|
||||
if len(dialogs) > 0 {
|
||||
s.dialogs = dialogs[0]
|
||||
|
|
|
|||
|
|
@ -569,7 +569,7 @@ func TestMessageStoreListByUserSupportsForwardAndAroundHistoryOffsets(t *testing
|
|||
if err != nil {
|
||||
t.Fatalf("around history: %v", err)
|
||||
}
|
||||
if got := messageIDs(around.Messages); !sameInts(got, []int{6, 5, 4, 3, 2, 1}) {
|
||||
if got := messageIDs(around.Messages); !sameInts(got, []int{5, 4, 3, 2, 1}) {
|
||||
t.Fatalf("around ids = %v, want unread/newer side plus older context", got)
|
||||
}
|
||||
|
||||
|
|
@ -583,8 +583,8 @@ func TestMessageStoreListByUserSupportsForwardAndAroundHistoryOffsets(t *testing
|
|||
if err != nil {
|
||||
t.Fatalf("forward history: %v", err)
|
||||
}
|
||||
if got := messageIDs(forward.Messages); !sameInts(got, []int{6, 5, 4}) {
|
||||
t.Fatalf("forward ids = %v, want messages newer than offset", got)
|
||||
if got := messageIDs(forward.Messages); !sameInts(got, []int{5, 4, 3}) {
|
||||
t.Fatalf("forward ids = %v, want forward window including offset anchor", got)
|
||||
}
|
||||
|
||||
hugePositive, err := messages.ListByUser(ctx, bobID, domain.MessageFilter{
|
||||
|
|
@ -610,8 +610,8 @@ func TestMessageStoreListByUserSupportsForwardAndAroundHistoryOffsets(t *testing
|
|||
if err != nil {
|
||||
t.Fatalf("huge negative add_offset history: %v", err)
|
||||
}
|
||||
if got := messageIDs(hugeNegative.Messages); !sameInts(got, []int{6, 5, 4}) {
|
||||
t.Fatalf("huge negative add_offset ids = %v, want clamped forward page", got)
|
||||
if got := messageIDs(hugeNegative.Messages); !sameInts(got, nil) {
|
||||
t.Fatalf("huge negative add_offset ids = %v, want empty page after clamped forward gap", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,15 @@ type UpdateStateStore struct {
|
|||
|
||||
// UpdateEventStore 是 store.UpdateEventStore 的内存实现。
|
||||
type UpdateEventStore struct {
|
||||
mu sync.RWMutex
|
||||
events map[int64][]domain.UpdateEvent
|
||||
mu sync.RWMutex
|
||||
events map[int64][]domain.UpdateEvent
|
||||
dispatches map[int64][]memoryUpdateDispatch
|
||||
}
|
||||
|
||||
type memoryUpdateDispatch struct {
|
||||
Pts int
|
||||
ExcludeAuthKeyID [8]byte
|
||||
ExcludeSessionID int64
|
||||
}
|
||||
|
||||
type updateStateKey struct {
|
||||
|
|
@ -27,29 +34,46 @@ type updateStateKey struct {
|
|||
|
||||
// NewUpdateEventStore 创建内存 UpdateEventStore。
|
||||
func NewUpdateEventStore() *UpdateEventStore {
|
||||
return &UpdateEventStore{events: make(map[int64][]domain.UpdateEvent)}
|
||||
return &UpdateEventStore{
|
||||
events: make(map[int64][]domain.UpdateEvent),
|
||||
dispatches: make(map[int64][]memoryUpdateDispatch),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UpdateEventStore) Append(_ context.Context, userID int64, event domain.UpdateEvent) error {
|
||||
_, err := s.append(userID, event, false)
|
||||
_, err := s.append(userID, event, false, false, [8]byte{}, 0)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *UpdateEventStore) AppendAllocated(_ context.Context, userID int64, event domain.UpdateEvent) (domain.UpdateEvent, error) {
|
||||
return s.append(userID, event, true)
|
||||
return s.append(userID, event, true, false, [8]byte{}, 0)
|
||||
}
|
||||
|
||||
func (s *UpdateEventStore) AppendAllocatedWithDispatch(_ context.Context, userID int64, event domain.UpdateEvent, _ [8]byte, _ int64) (domain.UpdateEvent, error) {
|
||||
return s.append(userID, event, true)
|
||||
func (s *UpdateEventStore) AppendAllocatedWithDispatch(_ context.Context, userID int64, event domain.UpdateEvent, excludeAuthKeyID [8]byte, excludeSessionID int64) (domain.UpdateEvent, error) {
|
||||
return s.append(userID, event, true, true, excludeAuthKeyID, excludeSessionID)
|
||||
}
|
||||
|
||||
func (s *UpdateEventStore) append(userID int64, event domain.UpdateEvent, allocate bool) (domain.UpdateEvent, error) {
|
||||
func (s *UpdateEventStore) append(userID int64, event domain.UpdateEvent, allocate, withDispatch bool, excludeAuthKeyID [8]byte, excludeSessionID int64) (domain.UpdateEvent, error) {
|
||||
if withDispatch && (excludeAuthKeyID == ([8]byte{})) != (excludeSessionID == 0) {
|
||||
return domain.UpdateEvent{}, fmt.Errorf("update dispatch exclusion requires both raw auth key and session id")
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
event = s.appendLocked(userID, event, allocate)
|
||||
if withDispatch {
|
||||
s.dispatches[userID] = append(s.dispatches[userID], memoryUpdateDispatch{
|
||||
Pts: event.Pts, ExcludeAuthKeyID: excludeAuthKeyID, ExcludeSessionID: excludeSessionID,
|
||||
})
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func (s *UpdateEventStore) appendLocked(userID int64, event domain.UpdateEvent, allocate bool) domain.UpdateEvent {
|
||||
if event.PtsCount <= 0 {
|
||||
event.PtsCount = 1
|
||||
}
|
||||
event.UserID = userID
|
||||
event = cloneUpdateEvent(event)
|
||||
s.mu.Lock()
|
||||
if allocate {
|
||||
current := 0
|
||||
for _, item := range s.events[userID] {
|
||||
|
|
@ -60,8 +84,7 @@ func (s *UpdateEventStore) append(userID int64, event domain.UpdateEvent, alloca
|
|||
event.Pts = current + event.PtsCount
|
||||
}
|
||||
s.events[userID] = append(s.events[userID], event)
|
||||
s.mu.Unlock()
|
||||
return event, nil
|
||||
return event
|
||||
}
|
||||
|
||||
func (s *UpdateEventStore) ListAfter(_ context.Context, userID int64, pts, limit int) ([]domain.UpdateEvent, error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue