fix: align private and channel update semantics

(cherry picked from commit c65f76f56278f74082c4fa792ed49104d5d33c38)
This commit is contained in:
A 2026-06-07 22:22:23 +08:00
parent dce7b92772
commit d84fa6e126
36 changed files with 1765 additions and 382 deletions

View file

@ -2700,12 +2700,15 @@ func (r *Router) channelOperationUpdates(ctx context.Context, viewerUserID int64
for _, member := range res.Members {
users = append(users, member.UserID)
}
updates := make([]tg.UpdateClass, 0, 1)
updates := make([]tg.UpdateClass, 0, 2)
if res.Event.Pts != 0 {
if update := tgChannelUpdate(viewerUserID, res.Event); update != nil {
updates = append(updates, update)
}
}
if res.Channel.ID != 0 {
updates = append(updates, &tg.UpdateChannel{ChannelID: res.Channel.ID})
}
return &tg.Updates{
Updates: updates,
Users: r.tgUsersForIDs(ctx, viewerUserID, users),

View file

@ -31,6 +31,8 @@ func (r *Router) registerContacts(d *tg.ServerDispatcher) {
d.OnContactsImportContacts(r.onContactsImportContacts)
d.OnContactsAddContact(r.onContactsAddContact)
d.OnContactsDeleteContacts(r.onContactsDeleteContacts)
d.OnContactsBlock(r.onContactsBlock)
d.OnContactsUnblock(r.onContactsUnblock)
d.OnContactsUpdateContactNote(r.onContactsUpdateContactNote)
d.OnContactsSearch(r.onContactsSearch)
d.OnContactsResolveUsername(r.onContactsResolveUsername)
@ -38,12 +40,7 @@ func (r *Router) registerContacts(d *tg.ServerDispatcher) {
d.OnContactsGetTopPeers(func(ctx context.Context, req *tg.ContactsGetTopPeersRequest) (tg.ContactsTopPeersClass, error) {
return tdesktop.TopPeers(), nil
})
d.OnContactsGetBlocked(func(ctx context.Context, req *tg.ContactsGetBlockedRequest) (tg.ContactsBlockedClass, error) {
if req.Limit > 50 {
return nil, limitInvalidErr()
}
return tdesktop.BlockedContacts(), nil
})
d.OnContactsGetBlocked(r.onContactsGetBlocked)
d.OnContactsGetSponsoredPeers(func(ctx context.Context, q string) (tg.ContactsSponsoredPeersClass, error) {
if utf8.RuneCountInString(q) > maxContactSearchQLen {
return nil, limitInvalidErr()
@ -52,6 +49,81 @@ func (r *Router) registerContacts(d *tg.ServerDispatcher) {
})
}
func (r *Router) onContactsBlock(ctx context.Context, req *tg.ContactsBlockRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
peer, ok := r.domainPeerFromInputPeer(userID, req.ID)
if !ok || peer.Type != domain.PeerTypeUser || peer.ID == 0 || peer.ID == userID {
return false, userIDInvalidErr()
}
if r.deps.Contacts == nil {
return true, nil
}
if _, err := r.deps.Contacts.BlockContact(ctx, userID, peer.ID, int(r.clock.Now().Unix())); err != nil {
return false, contactErr(err)
}
if settings, err := r.deps.Contacts.GetPeerSettings(ctx, userID, peer); err == nil {
_ = r.recordPeerSettings(ctx, userID, peer, settings)
}
return true, nil
}
func (r *Router) onContactsUnblock(ctx context.Context, req *tg.ContactsUnblockRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
peer, ok := r.domainPeerFromInputPeer(userID, req.ID)
if !ok || peer.Type != domain.PeerTypeUser || peer.ID == 0 || peer.ID == userID {
return false, userIDInvalidErr()
}
if r.deps.Contacts == nil {
return true, nil
}
if _, err := r.deps.Contacts.UnblockContact(ctx, userID, peer.ID); err != nil {
return false, contactErr(err)
}
if settings, err := r.deps.Contacts.GetPeerSettings(ctx, userID, peer); err == nil {
_ = r.recordPeerSettings(ctx, userID, peer, settings)
}
return true, nil
}
func (r *Router) onContactsGetBlocked(ctx context.Context, req *tg.ContactsGetBlockedRequest) (tg.ContactsBlockedClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if req.Limit > 100 || req.Offset < 0 {
return nil, limitInvalidErr()
}
if r.deps.Contacts == nil {
return tdesktop.BlockedContacts(), nil
}
list, err := r.deps.Contacts.GetBlocked(ctx, userID, req.Offset, req.Limit)
if err != nil {
return nil, internalErr()
}
blocked := make([]tg.PeerBlocked, 0, len(list.Blocked))
users := make([]tg.UserClass, 0, len(list.Blocked))
for _, item := range list.Blocked {
if item.User.ID == 0 {
continue
}
blocked = append(blocked, tg.PeerBlocked{
PeerID: &tg.PeerUser{UserID: item.User.ID},
Date: item.Date,
})
users = append(users, r.tgUser(item.User))
}
if list.Count > len(blocked)+req.Offset {
return &tg.ContactsBlockedSlice{Count: list.Count, Blocked: blocked, Chats: []tg.ChatClass{}, Users: users}, nil
}
return &tg.ContactsBlocked{Blocked: blocked, Chats: []tg.ChatClass{}, Users: users}, nil
}
func (r *Router) onContactsGetContacts(ctx context.Context, hash int64) (tg.ContactsContactsClass, error) {
if r.deps.Contacts == nil {
return &tg.ContactsContacts{}, nil

View file

@ -349,6 +349,16 @@ func tgUpdatesDifference(diff domain.UpdateDifference) tg.UpdatesDifferenceClass
}
}
}
for _, nudge := range diff.ChannelNudges {
if nudge.ChannelID == 0 {
continue
}
update := &tg.UpdateChannelTooLong{ChannelID: nudge.ChannelID}
if nudge.Pts > 0 {
update.SetPts(nudge.Pts)
}
out.OtherUpdates = append(out.OtherUpdates, update)
}
// Partial连续事件被 limit 截断、后面还有 → updates.differenceSlice客户端据 IntermediateState 续拉。
if diff.Partial {
return &tg.UpdatesDifferenceSlice{
@ -561,6 +571,15 @@ func tgOtherUpdateFromEvent(event domain.UpdateEvent) tg.UpdateClass {
}
case domain.UpdateEventReadHistoryOutbox:
return tgReadHistoryOutbox(event)
case domain.UpdateEventReadMessageContents:
if len(event.MessageIDs) == 0 {
return nil
}
return &tg.UpdateReadMessagesContents{
Messages: append([]int(nil), event.MessageIDs...),
Pts: event.Pts,
PtsCount: event.PtsCount,
}
case domain.UpdateEventEditMessage:
msg := tgMessage(event.Message)
if msg == nil {
@ -856,12 +875,13 @@ func tgMessage(m domain.Message) tg.MessageClass {
return nil
}
msg := &tg.Message{
Out: m.Out,
ID: m.ID,
PeerID: peer,
Date: m.Date,
Message: m.Body,
Entities: tgMessageEntities(m.Entities),
Out: m.Out,
MediaUnread: m.MediaUnread,
ID: m.ID,
PeerID: peer,
Date: m.Date,
Message: m.Body,
Entities: tgMessageEntities(m.Entities),
}
if m.EditDate != 0 {
msg.SetEditDate(m.EditDate)
@ -993,16 +1013,18 @@ func tgChannelMessage(viewerUserID int64, m domain.ChannelMessage) tg.MessageCla
return msg
}
msg := &tg.Message{
Out: outgoing,
Silent: m.Silent,
Post: m.Post,
Noforwards: m.NoForwards,
ID: m.ID,
FromID: from,
PeerID: peer,
Date: m.Date,
Message: m.Body,
Entities: tgMessageEntities(m.Entities),
Out: outgoing,
Silent: m.Silent,
Post: m.Post,
Noforwards: m.NoForwards,
Mentioned: m.Mentioned,
MediaUnread: m.MediaUnread,
ID: m.ID,
FromID: from,
PeerID: peer,
Date: m.Date,
Message: m.Body,
Entities: tgMessageEntities(m.Entities),
}
if m.EditDate != 0 {
msg.SetEditDate(m.EditDate)

View file

@ -138,6 +138,10 @@ type ContactsService interface {
DeleteContacts(ctx context.Context, userID int64, contactUserIDs []int64) (int, error)
UpdateContactNote(ctx context.Context, userID, contactUserID int64, note string, entities []domain.MessageEntity) (domain.Contact, error)
GetPeerSettings(ctx context.Context, userID int64, peer domain.Peer) (domain.PeerSettings, error)
BlockContact(ctx context.Context, userID, peerUserID int64, date int) (bool, error)
UnblockContact(ctx context.Context, userID, peerUserID int64) (bool, error)
IsBlocked(ctx context.Context, userID, peerUserID int64) (bool, error)
GetBlocked(ctx context.Context, userID int64, offset, limit int) (domain.BlockedContactList, error)
}
// DialogsService 抽象会话列表查询。
@ -278,6 +282,7 @@ type ChannelsService interface {
GetMessageReadParticipants(ctx context.Context, userID int64, req domain.ChannelReadParticipantsRequest) (domain.ChannelReadParticipantsResult, error)
GetDifference(ctx context.Context, userID int64, req domain.ChannelDifferenceRequest) (domain.ChannelDifference, error)
ActiveChannelIDsForUser(ctx context.Context, userID, afterChannelID int64, limit int) ([]int64, error)
DirtyActiveChannelsForUser(ctx context.Context, userID int64, sinceDate int, afterChannelID int64, limit int) ([]domain.DirtyChannel, error)
ActiveMemberIDs(ctx context.Context, userID, channelID int64, limit int) ([]int64, error)
InviteAdminMemberIDs(ctx context.Context, channelID int64, limit int) ([]int64, error)
FilterActiveMemberIDs(ctx context.Context, channelID int64, userIDs []int64) ([]int64, error)

View file

@ -140,6 +140,10 @@ func messageAuthorRequiredErr() error { return tgerr.New(403, "MESSAGE_AUTHOR_RE
func messageNotModifiedErr() error { return tgerr.New(400, "MESSAGE_NOT_MODIFIED") }
func messageEditForbiddenErr() error { return tgerr.New(403, "EDIT_MESSAGES_FORBIDDEN") }
func messageDeleteForbiddenErr() error { return tgerr.New(403, "DELETE_MESSAGES_FORBIDDEN") }
func messageNotReadYetErr() error { return tgerr.New(400, "MESSAGE_NOT_READ_YET") }
func replyMessageIDInvalidErr() error { return tgerr.New(400, "REPLY_MESSAGE_ID_INVALID") }

View file

@ -2808,6 +2808,7 @@ func (r *Router) onMessagesGetSponsoredMessages(ctx context.Context, req *tg.Mes
func (r *Router) onMessagesReadMessageContents(ctx context.Context, ids []int) (*tg.MessagesAffectedMessages, error) {
id, _ := AuthKeyIDFrom(ctx)
sessionID, _ := SessionIDFrom(ctx)
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
@ -2823,8 +2824,11 @@ func (r *Router) onMessagesReadMessageContents(ctx context.Context, ids []int) (
read := domain.ReadMessageContentsResult{OwnerUserID: userID}
if r.deps.Messages != nil {
read, err = r.deps.Messages.ReadMessageContents(ctx, userID, domain.ReadMessageContentsRequest{
OwnerUserID: userID,
IDs: ids,
OwnerUserID: userID,
IDs: ids,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: id,
OriginSessionID: sessionID,
})
if err != nil {
if errors.Is(err, domain.ErrMessageIDInvalid) {
@ -2833,12 +2837,15 @@ func (r *Router) onMessagesReadMessageContents(ctx context.Context, ids []int) (
return nil, internalErr()
}
}
affected, err := r.affectedMessages(ctx, id, userID)
if err != nil {
return nil, err
affected := &tg.MessagesAffectedMessages{Pts: read.Event.Pts, PtsCount: read.Event.PtsCount}
if read.Event.Pts == 0 {
affected, err = r.affectedMessages(ctx, id, userID)
if err != nil {
return nil, err
}
}
if contentIDs := readMessageContentIDs(read.MessageIDs); len(contentIDs) > 0 {
r.pushUserUpdates(ctx, userID, &tg.Updates{
r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateReadMessagesContents{
Messages: contentIDs,
Pts: affected.Pts,
@ -4378,6 +4385,10 @@ func (r *Router) onMessagesForwardMessages(ctx context.Context, req *tg.Messages
if r.deps.Channels == nil || r.deps.Messages == nil {
return nil, peerIDInvalidErr()
}
recipientBlocked, err := r.peerBlocksUser(ctx, userID, toPeer.ID)
if err != nil {
return nil, err
}
sources, err := r.forwardSources(ctx, userID, fromPeer, req.ID)
if err != nil {
return nil, messageForwardErr(err)
@ -4391,19 +4402,20 @@ func (r *Router) onMessagesForwardMessages(ctx context.Context, req *tg.Messages
forward = nil
}
sent, err := r.deps.Messages.SendPrivateText(ctx, userID, domain.SendPrivateTextRequest{
SenderUserID: userID,
RecipientUserID: toPeer.ID,
RandomID: req.RandomID[i],
Message: source.body,
Entities: source.entities,
Media: source.media,
Silent: req.Silent,
NoForwards: req.Noforwards,
ReplyTo: replyTo,
Forward: forward,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
SenderUserID: userID,
RecipientUserID: toPeer.ID,
RandomID: req.RandomID[i],
Message: source.body,
Entities: source.entities,
Media: source.media,
Silent: req.Silent,
NoForwards: req.Noforwards,
ReplyTo: replyTo,
Forward: forward,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
RecipientBlocked: recipientBlocked,
})
if err != nil {
return nil, messageForwardErr(err)
@ -4421,19 +4433,24 @@ func (r *Router) onMessagesForwardMessages(ctx context.Context, req *tg.Messages
}
sessionID, _ := SessionIDFrom(ctx)
authKeyID, _ := AuthKeyIDFrom(ctx)
recipientBlocked, err := r.peerBlocksUser(ctx, userID, toPeer.ID)
if err != nil {
return nil, err
}
res, err := r.deps.Messages.ForwardPrivateMessages(ctx, userID, domain.ForwardPrivateMessagesRequest{
OwnerUserID: userID,
FromPeer: fromPeer,
ToUserID: toPeer.ID,
MessageIDs: append([]int(nil), req.ID...),
RandomIDs: append([]int64(nil), req.RandomID...),
Silent: req.Silent,
NoForwards: req.Noforwards,
DropAuthor: req.DropAuthor,
ReplyTo: replyTo,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
OwnerUserID: userID,
FromPeer: fromPeer,
ToUserID: toPeer.ID,
MessageIDs: append([]int(nil), req.ID...),
RandomIDs: append([]int64(nil), req.RandomID...),
Silent: req.Silent,
NoForwards: req.Noforwards,
DropAuthor: req.DropAuthor,
ReplyTo: replyTo,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
RecipientBlocked: recipientBlocked,
})
if err != nil {
return nil, messageForwardErr(err)
@ -4641,6 +4658,13 @@ func (r *Router) onMessagesEditMessage(ctx context.Context, req *tg.MessagesEdit
if peer.Type != domain.PeerTypeUser || r.deps.Messages == nil {
return nil, peerIDInvalidErr()
}
blocked, err := r.peerBlocksUser(ctx, userID, peer.ID)
if err != nil {
return nil, err
}
if blocked {
return nil, messageEditForbiddenErr()
}
sessionID, _ := SessionIDFrom(ctx)
authKeyID, _ := AuthKeyIDFrom(ctx)
res, err := r.deps.Messages.EditMessage(ctx, userID, domain.EditMessageRequest{
@ -4818,6 +4842,19 @@ func (r *Router) onMessagesDeleteMessages(ctx context.Context, req *tg.MessagesD
if len(req.ID) > domain.MaxDeleteMessageIDs {
return nil, limitInvalidErr()
}
if req.GetRevoke() {
list, err := r.deps.Messages.GetMessages(ctx, userID, req.ID)
if err != nil {
return nil, internalErr()
}
blocked, err := r.messagesTouchBlockedPeer(ctx, userID, list.Messages)
if err != nil {
return nil, err
}
if blocked {
return nil, messageDeleteForbiddenErr()
}
}
sessionID, _ := SessionIDFrom(ctx)
res, err := r.deps.Messages.DeleteMessages(ctx, userID, domain.DeleteMessagesRequest{
OwnerUserID: userID,
@ -4892,6 +4929,15 @@ func (r *Router) onMessagesDeleteHistory(ctx context.Context, req *tg.MessagesDe
if r.deps.Messages == nil {
return r.affectedHistory(ctx, authKeyID, userID, 0)
}
if req.GetRevoke() {
blocked, err := r.peerBlocksUser(ctx, userID, peer.ID)
if err != nil {
return nil, err
}
if blocked {
return nil, messageDeleteForbiddenErr()
}
}
sessionID, _ := SessionIDFrom(ctx)
res, err := r.deps.Messages.DeleteHistory(ctx, userID, domain.DeleteHistoryRequest{
OwnerUserID: userID,
@ -4972,6 +5018,38 @@ func messageSendErr(err error) error {
}
}
func (r *Router) peerBlocksUser(ctx context.Context, userID, peerUserID int64) (bool, error) {
if userID == 0 || peerUserID == 0 || userID == peerUserID || r.deps.Contacts == nil {
return false, nil
}
blocked, err := r.deps.Contacts.IsBlocked(ctx, peerUserID, userID)
if err != nil {
return false, internalErr()
}
return blocked, nil
}
func (r *Router) messagesTouchBlockedPeer(ctx context.Context, userID int64, messages []domain.Message) (bool, error) {
seen := make(map[int64]struct{}, len(messages))
for _, msg := range messages {
if msg.Peer.Type != domain.PeerTypeUser || msg.Peer.ID == 0 {
continue
}
if _, ok := seen[msg.Peer.ID]; ok {
continue
}
seen[msg.Peer.ID] = struct{}{}
blocked, err := r.peerBlocksUser(ctx, userID, msg.Peer.ID)
if err != nil {
return false, err
}
if blocked {
return true, nil
}
}
return false, nil
}
func messageForwardErr(err error) error {
switch {
case errors.Is(err, domain.ErrMessageIDInvalid):

View file

@ -829,13 +829,16 @@ func TestMessagesCreateChatCreatesMegagroupAndDialogsRPC(t *testing.T) {
if !ok || !channel.Megagroup || channel.Broadcast {
t.Fatalf("chat = %#v, want megagroup channel", updates.Chats[0])
}
if len(updates.Updates) != 2 {
t.Fatalf("updates len = %d, want create + invite service messages", len(updates.Updates))
if len(updates.Updates) != 4 {
t.Fatalf("updates len = %d, want create/invite service messages plus channel refreshes", len(updates.Updates))
}
newMsg, ok := updates.Updates[0].(*tg.UpdateNewChannelMessage)
if !ok || newMsg.Pts != 1 || newMsg.PtsCount != 1 {
t.Fatalf("create update = %#v, want channel pts=1", updates.Updates[0])
}
if refresh, ok := updates.Updates[1].(*tg.UpdateChannel); !ok || refresh.ChannelID != channel.ID {
t.Fatalf("create refresh = %#v, want channel refresh", updates.Updates[1])
}
service, ok := newMsg.Message.(*tg.MessageService)
if !ok {
t.Fatalf("create message = %T, want service", newMsg.Message)
@ -843,9 +846,12 @@ func TestMessagesCreateChatCreatesMegagroupAndDialogsRPC(t *testing.T) {
if _, ok := service.Action.(*tg.MessageActionChannelCreate); !ok {
t.Fatalf("service action = %T, want channel create", service.Action)
}
inviteMsg, ok := updates.Updates[1].(*tg.UpdateNewChannelMessage)
inviteMsg, ok := updates.Updates[2].(*tg.UpdateNewChannelMessage)
if !ok || inviteMsg.Pts != 2 || inviteMsg.PtsCount != 1 {
t.Fatalf("invite update = %#v, want channel pts=2", updates.Updates[1])
t.Fatalf("invite update = %#v, want channel pts=2", updates.Updates[2])
}
if refresh, ok := updates.Updates[3].(*tg.UpdateChannel); !ok || refresh.ChannelID != channel.ID {
t.Fatalf("invite refresh = %#v, want channel refresh", updates.Updates[3])
}
inviteService, ok := inviteMsg.Message.(*tg.MessageService)
if !ok {
@ -1102,8 +1108,8 @@ func TestMessagesCreateChatDispatchRemembersTDesktopClientInfo(t *testing.T) {
if !ok || !channel.Megagroup || !channel.Creator {
t.Fatalf("second chat = %#v, want creator megagroup channel", updates.Chats[1])
}
if len(updates.Updates) != 2 {
t.Fatalf("updates len = %d, want create + invite service messages", len(updates.Updates))
if len(updates.Updates) != 4 {
t.Fatalf("updates len = %d, want create/invite service messages plus channel refreshes", len(updates.Updates))
}
participants, err := r.onChannelsGetParticipants(WithUserID(ctx, owner.ID), &tg.ChannelsGetParticipantsRequest{
Channel: &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
@ -4337,8 +4343,12 @@ func TestChannelAdminPinInviteRPC(t *testing.T) {
if err != nil {
t.Fatalf("import invite: %v", err)
}
if updates := imported.(*tg.Updates); len(updates.Chats) != 1 || len(updates.Updates) != 1 {
t.Fatalf("import updates = %+v, want chat and join service update", updates)
if updates := imported.(*tg.Updates); len(updates.Chats) != 1 || len(updates.Updates) != 2 {
t.Fatalf("import updates = %+v, want chat, join service update, and channel refresh", updates)
} else if _, ok := updates.Updates[0].(*tg.UpdateNewChannelMessage); !ok {
t.Fatalf("import first update = %T, want join service update", updates.Updates[0])
} else if refresh, ok := updates.Updates[1].(*tg.UpdateChannel); !ok || refresh.ChannelID != channel.ID {
t.Fatalf("import second update = %#v, want channel refresh", updates.Updates[1])
}
inviteList, err := r.onMessagesGetExportedChatInvites(WithUserID(ctx, friend.ID), &tg.MessagesGetExportedChatInvitesRequest{
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
@ -7202,6 +7212,29 @@ func TestUpdatesDifferenceIncludesDeleteMessages(t *testing.T) {
}
}
func TestUpdatesDifferenceIncludesChannelTooLongNudge(t *testing.T) {
got, ok := tgUpdatesDifference(domain.UpdateDifference{
State: domain.UpdateState{Pts: 8, Date: 1700000250, Seq: 0},
ChannelNudges: []domain.ChannelDifferenceNudge{{
ChannelID: 2000000001,
Pts: 12,
}},
}).(*tg.UpdatesDifference)
if !ok {
t.Fatalf("difference = %T, want *tg.UpdatesDifference", got)
}
if got.State.Pts != 8 || len(got.OtherUpdates) != 1 {
t.Fatalf("difference = %+v, want one channel nudge and account pts unchanged", got)
}
update, ok := got.OtherUpdates[0].(*tg.UpdateChannelTooLong)
if !ok || update.ChannelID != 2000000001 {
t.Fatalf("update = %T %+v, want UpdateChannelTooLong", got.OtherUpdates[0], got.OtherUpdates[0])
}
if pts, ok := update.GetPts(); !ok || pts != 12 {
t.Fatalf("channel nudge pts = %d set=%v, want 12", pts, ok)
}
}
func TestUpdatesDifferenceIncludesSettingsUpdates(t *testing.T) {
peer := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002}
got, ok := tgUpdatesDifference(domain.UpdateDifference{
@ -8354,6 +8387,137 @@ func TestMessagesSendMessageReturnsUpdateAndRecordsOwnerContext(t *testing.T) {
}
}
func TestContactsBlockGetBlockedAndUnblockRPC(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
alice, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550009001", FirstName: "Alice"})
if err != nil {
t.Fatalf("create alice: %v", err)
}
bob, err := userStore.Create(ctx, domain.User{AccessHash: 22, Phone: "15550009002", FirstName: "Bob"})
if err != nil {
t.Fatalf("create bob: %v", err)
}
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Contacts: appcontacts.NewService(memory.NewContactStore(), userStore),
}, zaptest.NewLogger(t), clock.System)
ok, err := r.onContactsBlock(WithUserID(ctx, bob.ID), &tg.ContactsBlockRequest{
ID: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash},
})
if err != nil || !ok {
t.Fatalf("contacts.block = %v, %v", ok, err)
}
blocked, err := r.onContactsGetBlocked(WithUserID(ctx, bob.ID), &tg.ContactsGetBlockedRequest{Limit: 10})
if err != nil {
t.Fatalf("contacts.getBlocked: %v", err)
}
full, ok := blocked.(*tg.ContactsBlocked)
if !ok || len(full.Blocked) != 1 || len(full.Users) != 1 {
t.Fatalf("blocked = %T %+v, want one blocked user", blocked, blocked)
}
if peer, ok := full.Blocked[0].PeerID.(*tg.PeerUser); !ok || peer.UserID != alice.ID {
t.Fatalf("blocked peer = %#v, want alice", full.Blocked[0].PeerID)
}
if user, ok := full.Users[0].(*tg.User); !ok || user.ID != alice.ID {
t.Fatalf("blocked user = %#v, want alice", full.Users[0])
}
ok, err = r.onContactsUnblock(WithUserID(ctx, bob.ID), &tg.ContactsUnblockRequest{
ID: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash},
})
if err != nil || !ok {
t.Fatalf("contacts.unblock = %v, %v", ok, err)
}
blocked, err = r.onContactsGetBlocked(WithUserID(ctx, bob.ID), &tg.ContactsGetBlockedRequest{Limit: 10})
if err != nil {
t.Fatalf("contacts.getBlocked after unblock: %v", err)
}
if full, ok := blocked.(*tg.ContactsBlocked); !ok || len(full.Blocked) != 0 {
t.Fatalf("blocked after unblock = %T %+v, want empty contacts.blocked", blocked, blocked)
}
}
func TestMessagesPrivateBlockPreventsRecipientInboxAndRevokeRPC(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
alice, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550009101", FirstName: "Alice"})
if err != nil {
t.Fatalf("create alice: %v", err)
}
bob, err := userStore.Create(ctx, domain.User{AccessHash: 22, Phone: "15550009102", FirstName: "Bob"})
if err != nil {
t.Fatalf("create bob: %v", err)
}
dialogs := memory.NewDialogStore()
messageStore := memory.NewMessageStore(dialogs)
contactStore := memory.NewContactStore()
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Contacts: appcontacts.NewService(contactStore, userStore),
Messages: appmessages.NewService(messageStore, dialogs),
Dialogs: appdialogs.NewService(dialogs),
}, zaptest.NewLogger(t), clock.System)
delivered, err := r.onMessagesSendMessage(WithUserID(ctx, alice.ID), &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
Message: "before block",
RandomID: 91001,
})
if err != nil {
t.Fatalf("send before block: %v", err)
}
deliveredUpdates := delivered.(*tg.Updates)
deliveredMsg := deliveredUpdates.Updates[1].(*tg.UpdateNewMessage).Message.(*tg.Message)
if ok, err := r.onContactsBlock(WithUserID(ctx, bob.ID), &tg.ContactsBlockRequest{
ID: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash},
}); err != nil || !ok {
t.Fatalf("bob block alice = %v, %v", ok, err)
}
blockedSend, err := r.onMessagesSendMessage(WithUserID(ctx, alice.ID), &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
Message: "after block",
RandomID: 91002,
})
if err != nil {
t.Fatalf("send after block: %v", err)
}
blockedUpdates := blockedSend.(*tg.Updates)
blockedMsg := blockedUpdates.Updates[1].(*tg.UpdateNewMessage).Message.(*tg.Message)
if blockedMsg.ID == 0 || !blockedMsg.Out {
t.Fatalf("blocked sender update = %#v, want outgoing sender message", blockedMsg)
}
bobHistory, err := messageStore.ListByUser(ctx, bob.ID, domain.MessageFilter{
HasPeer: true,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: alice.ID},
Limit: 10,
})
if err != nil {
t.Fatalf("bob history: %v", err)
}
if len(bobHistory.Messages) != 1 || bobHistory.Messages[0].Body != "before block" {
t.Fatalf("bob history = %+v, want only pre-block delivered message", bobHistory.Messages)
}
aliceHistory, err := messageStore.ListByUser(ctx, alice.ID, domain.MessageFilter{
HasPeer: true,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: bob.ID},
Limit: 10,
})
if err != nil {
t.Fatalf("alice history: %v", err)
}
if len(aliceHistory.Messages) != 2 {
t.Fatalf("alice history len = %d, want delivered + sender-only blocked message", len(aliceHistory.Messages))
}
deleteReq := &tg.MessagesDeleteMessagesRequest{ID: []int{deliveredMsg.ID}}
deleteReq.SetRevoke(true)
if _, err := r.onMessagesDeleteMessages(WithUserID(ctx, alice.ID), deleteReq); err == nil || !strings.Contains(err.Error(), "DELETE_MESSAGES_FORBIDDEN") {
t.Fatalf("revoke after block err = %v, want DELETE_MESSAGES_FORBIDDEN", err)
}
}
func TestMessagesSendMessageSupportsReplyAndFlags(t *testing.T) {
const (
senderID = int64(1000000001)

View file

@ -94,21 +94,26 @@ func (r *Router) sendOutgoing(ctx context.Context, userID int64, peer domain.Pee
if err != nil {
return nil, false, err
}
recipientBlocked, err := r.peerBlocksUser(ctx, userID, peer.ID)
if err != nil {
return nil, false, err
}
sessionID, _ := SessionIDFrom(ctx)
authKeyID, _ := AuthKeyIDFrom(ctx)
res, err := r.deps.Messages.SendPrivateText(ctx, userID, domain.SendPrivateTextRequest{
SenderUserID: userID,
RecipientUserID: peer.ID,
RandomID: p.randomID,
Message: p.message,
Entities: domainMessageEntities(p.entities),
Media: p.media,
Silent: p.silent,
NoForwards: p.noforwards,
ReplyTo: replyTo,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
SenderUserID: userID,
RecipientUserID: peer.ID,
RandomID: p.randomID,
Message: p.message,
Entities: domainMessageEntities(p.entities),
Media: p.media,
Silent: p.silent,
NoForwards: p.noforwards,
ReplyTo: replyTo,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
RecipientBlocked: recipientBlocked,
})
if err != nil {
return nil, false, messageSendErr(err)

View file

@ -63,13 +63,32 @@ func (r *Router) onUpdatesGetDifference(ctx context.Context, req *tg.UpdatesGetD
return nil, internalErr()
}
r.markSessionReceivesUpdates(ctx, userID)
if len(st.Events) == 0 {
st.ChannelNudges = r.accountChannelDifferenceNudges(ctx, userID, req.Date)
if len(st.Events) == 0 && len(st.ChannelNudges) == 0 {
return &tg.UpdatesDifferenceEmpty{Date: st.State.Date, Seq: st.State.Seq}, nil
}
st.Events = r.enrichUpdateEvents(ctx, userID, st.Events)
return tgUpdatesDifference(st), nil
}
func (r *Router) accountChannelDifferenceNudges(ctx context.Context, userID int64, sinceDate int) []domain.ChannelDifferenceNudge {
if r.deps.Channels == nil || userID == 0 || sinceDate <= 0 {
return nil
}
dirty, err := r.deps.Channels.DirtyActiveChannelsForUser(ctx, userID, sinceDate, 0, domain.MaxChannelDifferenceLimit)
if err != nil || len(dirty) == 0 {
return nil
}
out := make([]domain.ChannelDifferenceNudge, 0, len(dirty))
for _, item := range dirty {
if item.ChannelID == 0 {
continue
}
out = append(out, domain.ChannelDifferenceNudge{ChannelID: item.ChannelID, Pts: item.Pts})
}
return out
}
func (r *Router) markSessionReceivesUpdates(ctx context.Context, userID int64) {
if r.deps.Sessions == nil {
return