feat: sync iOS compatibility support
This commit is contained in:
parent
1f646ef024
commit
50803a604c
32 changed files with 871 additions and 78 deletions
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/gotd/td/tg"
|
||||
|
||||
ioscompat "telesrv/internal/compat/ios"
|
||||
"telesrv/internal/compat/tdesktop"
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
|
@ -19,6 +20,12 @@ func (r *Router) registerAccount(d *tg.ServerDispatcher) {
|
|||
d.OnAccountUnregisterDevice(func(ctx context.Context, req *tg.AccountUnregisterDeviceRequest) (bool, error) {
|
||||
return true, nil
|
||||
})
|
||||
d.OnAccountUpdateDeviceLocked(func(ctx context.Context, period int) (bool, error) {
|
||||
if _, _, err := r.currentUserID(ctx); err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
return ioscompat.DeviceLockedUpdated(), nil
|
||||
})
|
||||
d.OnAccountSendChangePhoneCode(r.onAccountSendChangePhoneCode)
|
||||
d.OnAccountChangePhone(r.onAccountChangePhone)
|
||||
d.OnAccountCheckUsername(r.onAccountCheckUsername)
|
||||
|
|
|
|||
|
|
@ -916,6 +916,11 @@ func TestChannelUsernameAndManagementRPC(t *testing.T) {
|
|||
t.Fatalf("toggle forum chat = %+v, want forum+forum_tabs", forumUpdates.(*tg.Updates).Chats[0])
|
||||
}
|
||||
forumPeer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
|
||||
if _, err := r.onMessagesGetForumTopicsByID(WithUserID(ctx, owner.ID), &tg.MessagesGetForumTopicsByIDRequest{
|
||||
Peer: forumPeer,
|
||||
}); err == nil || !strings.Contains(err.Error(), "TOPICS_EMPTY") {
|
||||
t.Fatalf("messages.getForumTopicsByID empty topics err = %v, want TOPICS_EMPTY", err)
|
||||
}
|
||||
forumTopics, err := r.onMessagesGetForumTopics(WithUserID(ctx, owner.ID), &tg.MessagesGetForumTopicsRequest{
|
||||
Peer: forumPeer,
|
||||
Limit: 10,
|
||||
|
|
@ -1001,15 +1006,25 @@ func TestChannelUsernameAndManagementRPC(t *testing.T) {
|
|||
if !foundCreated {
|
||||
t.Fatalf("messages.getForumTopics topics = %+v, want created topic id %d", forumTopicsWithCreated.Topics, topicID)
|
||||
}
|
||||
missingTopicID := topicID + 1000
|
||||
forumTopicsByID, err = r.onMessagesGetForumTopicsByID(WithUserID(ctx, owner.ID), &tg.MessagesGetForumTopicsByIDRequest{
|
||||
Peer: forumPeer,
|
||||
Topics: []int{forumGeneralTopicID, topicID},
|
||||
Topics: []int{topicID, missingTopicID, forumGeneralTopicID, topicID},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("messages.getForumTopicsByID with created topic: %v", err)
|
||||
}
|
||||
if forumTopicsByID.Count != 2 || len(forumTopicsByID.Topics) != 2 || len(forumTopicsByID.Messages) == 0 {
|
||||
t.Fatalf("messages.getForumTopicsByID with created topic = %+v, want General + created topic", forumTopicsByID)
|
||||
if forumTopicsByID.Count != 3 || len(forumTopicsByID.Topics) != 3 || len(forumTopicsByID.Messages) == 0 {
|
||||
t.Fatalf("messages.getForumTopicsByID with created/missing/duplicate topics = %+v, want three unique results", forumTopicsByID)
|
||||
}
|
||||
if topic, ok := forumTopicsByID.Topics[0].(*tg.ForumTopic); !ok || topic.ID != topicID {
|
||||
t.Fatalf("messages.getForumTopicsByID result[0] = %T %+v, want live topic %d", forumTopicsByID.Topics[0], forumTopicsByID.Topics[0], topicID)
|
||||
}
|
||||
if topic, ok := forumTopicsByID.Topics[1].(*tg.ForumTopicDeleted); !ok || topic.ID != missingTopicID {
|
||||
t.Fatalf("messages.getForumTopicsByID result[1] = %T %+v, want deleted placeholder %d", forumTopicsByID.Topics[1], forumTopicsByID.Topics[1], missingTopicID)
|
||||
}
|
||||
if topic, ok := forumTopicsByID.Topics[2].(*tg.ForumTopic); !ok || topic.ID != forumGeneralTopicID {
|
||||
t.Fatalf("messages.getForumTopicsByID result[2] = %T %+v, want General", forumTopicsByID.Topics[2], forumTopicsByID.Topics[2])
|
||||
}
|
||||
topicReply := &tg.InputReplyToMessage{ReplyToMsgID: 0}
|
||||
topicReply.SetTopMsgID(topicID)
|
||||
|
|
@ -1217,6 +1232,19 @@ func TestChannelUsernameAndManagementRPC(t *testing.T) {
|
|||
if forumTopicsAfterDelete.Count != 1 || len(forumTopicsAfterDelete.Topics) != 1 {
|
||||
t.Fatalf("messages.getForumTopics after delete topic = %+v, want General only", forumTopicsAfterDelete)
|
||||
}
|
||||
deletedByID, err := r.onMessagesGetForumTopicsByID(WithUserID(ctx, owner.ID), &tg.MessagesGetForumTopicsByIDRequest{
|
||||
Peer: forumPeer,
|
||||
Topics: []int{topicID},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("messages.getForumTopicsByID after delete topic: %v", err)
|
||||
}
|
||||
if deletedByID.Count != 1 || len(deletedByID.Topics) != 1 {
|
||||
t.Fatalf("messages.getForumTopicsByID after delete = %+v, want one deleted placeholder", deletedByID)
|
||||
}
|
||||
if topic, ok := deletedByID.Topics[0].(*tg.ForumTopicDeleted); !ok || topic.ID != topicID {
|
||||
t.Fatalf("messages.getForumTopicsByID after delete result = %T %+v, want deleted topic %d", deletedByID.Topics[0], deletedByID.Topics[0], topicID)
|
||||
}
|
||||
antiSpamUpdates, err := r.onChannelsToggleAntiSpam(WithUserID(ctx, owner.ID), &tg.ChannelsToggleAntiSpamRequest{Channel: input, Enabled: true})
|
||||
if err != nil {
|
||||
t.Fatalf("toggle antispam: %v", err)
|
||||
|
|
|
|||
|
|
@ -38,9 +38,13 @@ var androidSDKVersionRE = regexp.MustCompile(`\bsdk\s+\d+\b`)
|
|||
type ClientType string
|
||||
|
||||
const (
|
||||
ClientTypeUnknown ClientType = "unknown"
|
||||
ClientTypeTDesktop ClientType = "tdesktop"
|
||||
ClientTypeAndroid ClientType = "android"
|
||||
ClientTypeUnknown ClientType = "unknown"
|
||||
ClientTypeTDesktop ClientType = "tdesktop"
|
||||
ClientTypeAndroid ClientType = "android"
|
||||
ClientTypeIOS ClientType = "ios"
|
||||
ClientTypeMacOS ClientType = "macos"
|
||||
ClientTypeTWeb ClientType = "tweb"
|
||||
ClientTypeTelegramTT ClientType = "telegram-tt"
|
||||
)
|
||||
|
||||
// ClientInfo 是 initConnection 携带的客户端信息。
|
||||
|
|
@ -53,6 +57,10 @@ type ClientInfo struct {
|
|||
LangPack string
|
||||
LangCode string
|
||||
Type ClientType
|
||||
// typeResolved distinguishes current-connection classification from raw
|
||||
// metadata restored from storage. A persisted unknown remains unknown until
|
||||
// a fresh initConnection supplies authoritative wire evidence.
|
||||
typeResolved bool
|
||||
}
|
||||
|
||||
// WithLayer 在 ctx 注入客户端 layer(来自 invokeWithLayer)。
|
||||
|
|
@ -88,22 +96,32 @@ func ClientTypeFrom(ctx context.Context) ClientType {
|
|||
}
|
||||
|
||||
func normalizeClientInfo(info ClientInfo) ClientInfo {
|
||||
if !knownClientType(info.Type) {
|
||||
info.Type = detectClientType(info)
|
||||
}
|
||||
info.Type = detectClientType(info)
|
||||
info.typeResolved = true
|
||||
return info
|
||||
}
|
||||
|
||||
func (info ClientInfo) ClientType() ClientType {
|
||||
if knownClientType(info.Type) {
|
||||
return info.Type
|
||||
if info.typeResolved {
|
||||
if knownClientType(info.Type) {
|
||||
return info.Type
|
||||
}
|
||||
return ClientTypeUnknown
|
||||
}
|
||||
return detectClientType(info)
|
||||
}
|
||||
|
||||
func restoreClientInfo(info ClientInfo) ClientInfo {
|
||||
if !knownClientType(info.Type) {
|
||||
info.Type = ClientTypeUnknown
|
||||
}
|
||||
info.typeResolved = true
|
||||
return info
|
||||
}
|
||||
|
||||
func knownClientType(t ClientType) bool {
|
||||
switch t {
|
||||
case ClientTypeTDesktop, ClientTypeAndroid:
|
||||
case ClientTypeTDesktop, ClientTypeAndroid, ClientTypeIOS, ClientTypeMacOS, ClientTypeTWeb, ClientTypeTelegramTT:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
|
@ -118,25 +136,55 @@ func clientTypeFromAPIID(apiID int) ClientType {
|
|||
return ClientTypeAndroid
|
||||
case 2040, 17349, 611335:
|
||||
return ClientTypeTDesktop
|
||||
case 8:
|
||||
return ClientTypeIOS
|
||||
case 2496, 1025907:
|
||||
return ClientTypeTWeb
|
||||
default:
|
||||
return ClientTypeUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func detectClientType(info ClientInfo) ClientType {
|
||||
if t := clientTypeFromAPIID(info.APIID); t != ClientTypeUnknown {
|
||||
// Wire evidence wins over stored/explicit type and API id. In particular,
|
||||
// local TWeb builds may reuse api_id=2040 (the official TDesktop id), while
|
||||
// lang_pack=webk and a browser UA unambiguously identify the web client.
|
||||
if t := clientTypeFromStrongEvidence(info); t != ClientTypeUnknown {
|
||||
return t
|
||||
}
|
||||
if strings.EqualFold(info.LangPack, string(ClientTypeAndroid)) {
|
||||
return ClientTypeAndroid
|
||||
if knownClientType(info.Type) {
|
||||
return info.Type
|
||||
}
|
||||
if strings.EqualFold(info.LangPack, string(ClientTypeTDesktop)) {
|
||||
return clientTypeFromAPIID(info.APIID)
|
||||
}
|
||||
|
||||
func clientTypeFromStrongEvidence(info ClientInfo) ClientType {
|
||||
langPack := strings.ToLower(strings.TrimSpace(info.LangPack))
|
||||
switch langPack {
|
||||
case "weba":
|
||||
return ClientTypeTelegramTT
|
||||
case "web", "webk":
|
||||
return ClientTypeTWeb
|
||||
case string(ClientTypeAndroid):
|
||||
return ClientTypeAndroid
|
||||
case string(ClientTypeIOS):
|
||||
return ClientTypeIOS
|
||||
case string(ClientTypeMacOS):
|
||||
return ClientTypeMacOS
|
||||
case string(ClientTypeTDesktop):
|
||||
return ClientTypeTDesktop
|
||||
}
|
||||
client := strings.ToLower(info.DeviceModel + " " + info.SystemVersion + " " + info.AppVersion)
|
||||
switch {
|
||||
case strings.Contains(client, "mozilla/"), strings.Contains(client, "applewebkit/"),
|
||||
strings.Contains(client, "telegram web"), strings.Contains(client, "webogram"):
|
||||
return ClientTypeTWeb
|
||||
case strings.Contains(client, "android"), androidSDKVersionRE.MatchString(client):
|
||||
return ClientTypeAndroid
|
||||
case strings.Contains(client, "iphone"), strings.Contains(client, "ipad"),
|
||||
strings.Contains(client, "ipod"), strings.Contains(client, "ipados"),
|
||||
strings.Contains(client, "ios "):
|
||||
return ClientTypeIOS
|
||||
case strings.Contains(client, "tdesktop"), strings.Contains(client, "desktop"):
|
||||
return ClientTypeTDesktop
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -290,6 +290,7 @@ func channelForumMissingErr() error { return tgerr.New(400, "CHANNEL_FORUM_MISSI
|
|||
|
||||
func topicTitleEmptyErr() error { return tgerr.New(400, "TOPIC_TITLE_EMPTY") }
|
||||
func topicIDInvalidErr() error { return tgerr.New(400, "TOPIC_ID_INVALID") }
|
||||
func topicsEmptyErr() error { return tgerr.New(400, "TOPICS_EMPTY") }
|
||||
|
||||
// randomIDEmptyErr 表示发送消息缺少 random_id。
|
||||
func randomIDEmptyErr() error { return tgerr.New(400, "RANDOM_ID_EMPTY") }
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/gotd/td/tg"
|
||||
|
||||
androidcompat "telesrv/internal/compat/android"
|
||||
ioscompat "telesrv/internal/compat/ios"
|
||||
"telesrv/internal/compat/tdesktop"
|
||||
)
|
||||
|
||||
|
|
@ -21,6 +22,12 @@ func (r *Router) registerHelp(d *tg.ServerDispatcher) {
|
|||
d.OnHelpGetInviteText(func(ctx context.Context) (*tg.HelpInviteText, error) {
|
||||
return &tg.HelpInviteText{Message: "Join me on Telegram."}, nil
|
||||
})
|
||||
d.OnHelpGetAppUpdate(func(ctx context.Context, source string) (tg.HelpAppUpdateClass, error) {
|
||||
if _, _, err := r.currentUserID(ctx); err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
return ioscompat.NoAppUpdate(), nil
|
||||
})
|
||||
d.OnHelpGetAppConfig(func(ctx context.Context, hash int) (tg.HelpAppConfigClass, error) {
|
||||
if r.deps.Help == nil {
|
||||
return tdesktop.AppConfig(hash), nil
|
||||
|
|
|
|||
|
|
@ -143,6 +143,14 @@ func langPackFromClient(ctx context.Context) string {
|
|||
return string(ClientTypeAndroid)
|
||||
case ClientTypeTDesktop:
|
||||
return string(ClientTypeTDesktop)
|
||||
case ClientTypeIOS:
|
||||
return string(ClientTypeIOS)
|
||||
case ClientTypeMacOS:
|
||||
return string(ClientTypeMacOS)
|
||||
case ClientTypeTWeb:
|
||||
return "webk"
|
||||
case ClientTypeTelegramTT:
|
||||
return "weba"
|
||||
}
|
||||
client := strings.ToLower(info.DeviceModel + " " + info.SystemVersion + " " + info.AppVersion)
|
||||
if strings.Contains(client, "android") {
|
||||
|
|
|
|||
|
|
@ -321,6 +321,9 @@ func (r *Router) onMessagesGetForumTopicsByID(ctx context.Context, req *tg.Messa
|
|||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if len(req.Topics) == 0 {
|
||||
return nil, topicsEmptyErr()
|
||||
}
|
||||
if len(req.Topics) > maxForumTopicIDs {
|
||||
return nil, limitInvalidErr()
|
||||
}
|
||||
|
|
@ -332,11 +335,18 @@ func (r *Router) onMessagesGetForumTopicsByID(ctx context.Context, req *tg.Messa
|
|||
return nil, channelForumMissingErr()
|
||||
}
|
||||
includeGeneral := false
|
||||
requestedIDs := make([]int, 0, len(req.Topics))
|
||||
ids := make([]int, 0, len(req.Topics))
|
||||
seen := make(map[int]struct{}, len(req.Topics))
|
||||
for _, topicID := range req.Topics {
|
||||
if topicID <= 0 || topicID > domain.MaxMessageBoxID {
|
||||
return nil, messageIDInvalidErr()
|
||||
}
|
||||
if _, ok := seen[topicID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[topicID] = struct{}{}
|
||||
requestedIDs = append(requestedIDs, topicID)
|
||||
if topicID == forumGeneralTopicID {
|
||||
includeGeneral = true
|
||||
continue
|
||||
|
|
@ -350,7 +360,34 @@ func (r *Router) onMessagesGetForumTopicsByID(ctx context.Context, req *tg.Messa
|
|||
return nil, forumTopicError(err)
|
||||
}
|
||||
}
|
||||
return r.forumTopicsResponse(ctx, userID, view, list, includeGeneral), nil
|
||||
return r.forumTopicsByIDResponse(ctx, userID, view, list, includeGeneral, requestedIDs), nil
|
||||
}
|
||||
|
||||
// forumTopicsByIDResponse keeps the request's unique ID order and returns one
|
||||
// constructor for every requested topic. Telegram clients use
|
||||
// forumTopicDeleted as a positive deletion/missing confirmation; silently
|
||||
// omitting an ID leaves their local thread state stale and causes repeat reads.
|
||||
func (r *Router) forumTopicsByIDResponse(ctx context.Context, userID int64, view domain.ChannelView, list domain.ChannelForumTopicList, includeGeneral bool, requestedIDs []int) *tg.MessagesForumTopics {
|
||||
response := r.forumTopicsResponse(ctx, userID, view, list, includeGeneral)
|
||||
live := make(map[int]tg.ForumTopicClass, len(response.Topics))
|
||||
for _, topic := range response.Topics {
|
||||
switch topic := topic.(type) {
|
||||
case *tg.ForumTopic:
|
||||
live[topic.ID] = topic
|
||||
case *tg.ForumTopicDeleted:
|
||||
live[topic.ID] = topic
|
||||
}
|
||||
}
|
||||
response.Topics = make([]tg.ForumTopicClass, 0, len(requestedIDs))
|
||||
for _, topicID := range requestedIDs {
|
||||
if topic, ok := live[topicID]; ok {
|
||||
response.Topics = append(response.Topics, topic)
|
||||
} else {
|
||||
response.Topics = append(response.Topics, &tg.ForumTopicDeleted{ID: topicID})
|
||||
}
|
||||
}
|
||||
response.Count = len(response.Topics)
|
||||
return response
|
||||
}
|
||||
|
||||
func (r *Router) forumTopicPeerView(ctx context.Context, userID int64, peer tg.InputPeerClass) (domain.ChannelView, error) {
|
||||
|
|
|
|||
57
internal/rpc/messages_recent_locations.go
Normal file
57
internal/rpc/messages_recent_locations.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// onMessagesGetRecentLocations returns the peer's newest live-location
|
||||
// messages through the existing media seek indexes. Expired/stopped items are
|
||||
// deliberately retained: iOS tags every messageMediaGeoLive in its local
|
||||
// history and applies the active-period check in PeerLiveLocationsContext.
|
||||
func (r *Router) onMessagesGetRecentLocations(ctx context.Context, req *tg.MessagesGetRecentLocationsRequest) (tg.MessagesMessagesClass, error) {
|
||||
if req == nil || req.Limit < 0 || req.Limit > maxSearchResultsLimit {
|
||||
return nil, limitInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if input, ok := req.Peer.(*tg.InputPeerUser); ok && input != nil {
|
||||
if err := r.validateInputUser(ctx, &tg.InputUser{UserID: input.UserID, AccessHash: input.AccessHash}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
search := domain.MediaSearchRequest{
|
||||
Categories: []domain.MediaCategory{domain.MediaCategoryGeoLive},
|
||||
Limit: req.Limit,
|
||||
}
|
||||
if peer.Type == domain.PeerTypeChannel {
|
||||
if r.deps.Channels == nil {
|
||||
return &tg.MessagesMessages{Messages: []tg.MessageClass{}, Chats: []tg.ChatClass{}, Users: []tg.UserClass{}}, nil
|
||||
}
|
||||
history, err := r.deps.Channels.SearchChannelMedia(ctx, userID, peer.ID, search)
|
||||
if err != nil {
|
||||
return nil, channelInvalidErr(err)
|
||||
}
|
||||
history = r.enrichChannelHistory(ctx, userID, history)
|
||||
r.trackChannelInterest(ctx, userID, peer.ID)
|
||||
return r.tgChannelHistoryMessages(ctx, userID, history), nil
|
||||
}
|
||||
r.clearChannelInterest(ctx, userID)
|
||||
if r.deps.Messages == nil {
|
||||
return &tg.MessagesMessages{Messages: []tg.MessageClass{}, Chats: []tg.ChatClass{}, Users: []tg.UserClass{}}, nil
|
||||
}
|
||||
list, err := r.deps.Messages.SearchPrivateMedia(ctx, userID, peer.ID, search)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
return r.tgMessagesMessages(ctx, userID, r.enrichMessageList(ctx, userID, list)), nil
|
||||
}
|
||||
61
internal/rpc/messages_recent_locations_test.go
Normal file
61
internal/rpc/messages_recent_locations_test.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/gotd/td/tgerr"
|
||||
)
|
||||
|
||||
func TestMessagesGetRecentLocationsReturnsOnlyGeoLive(t *testing.T) {
|
||||
r, owner, friend := newMediaTestRouter(t)
|
||||
ctx := WithUserID(context.Background(), owner.ID)
|
||||
peer := &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}
|
||||
|
||||
if _, err := r.onMessagesSendMessage(ctx, &tg.MessagesSendMessageRequest{
|
||||
Peer: peer, Message: "not a location", RandomID: 73001,
|
||||
}); err != nil {
|
||||
t.Fatalf("send text: %v", err)
|
||||
}
|
||||
want := sendTestLiveLocation(t, r, owner.ID, peer, 73002, 900)
|
||||
|
||||
result, err := r.onMessagesGetRecentLocations(ctx, &tg.MessagesGetRecentLocationsRequest{Peer: peer, Limit: 20})
|
||||
if err != nil {
|
||||
t.Fatalf("getRecentLocations: %v", err)
|
||||
}
|
||||
var messages []tg.MessageClass
|
||||
switch value := result.(type) {
|
||||
case *tg.MessagesMessages:
|
||||
messages = value.Messages
|
||||
case *tg.MessagesMessagesSlice:
|
||||
messages = value.Messages
|
||||
default:
|
||||
t.Fatalf("getRecentLocations = %T", result)
|
||||
}
|
||||
if len(messages) != 1 {
|
||||
t.Fatalf("recent location messages = %d, want 1", len(messages))
|
||||
}
|
||||
got, ok := messages[0].(*tg.Message)
|
||||
if !ok || got.ID != want.ID {
|
||||
t.Fatalf("recent location = %#v, want id %d", messages[0], want.ID)
|
||||
}
|
||||
if _, ok := got.Media.(*tg.MessageMediaGeoLive); !ok {
|
||||
t.Fatalf("recent location media = %T, want MessageMediaGeoLive", got.Media)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesGetRecentLocationsValidatesLimitAndAccessHash(t *testing.T) {
|
||||
r, owner, friend := newMediaTestRouter(t)
|
||||
ctx := WithUserID(context.Background(), owner.ID)
|
||||
if _, err := r.onMessagesGetRecentLocations(ctx, &tg.MessagesGetRecentLocationsRequest{
|
||||
Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}, Limit: maxSearchResultsLimit + 1,
|
||||
}); err == nil || !tgerr.Is(err, "LIMIT_INVALID") {
|
||||
t.Fatalf("oversized limit err = %v, want LIMIT_INVALID", err)
|
||||
}
|
||||
if _, err := r.onMessagesGetRecentLocations(ctx, &tg.MessagesGetRecentLocationsRequest{
|
||||
Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash + 1}, Limit: 20,
|
||||
}); err == nil || !tgerr.Is(err, "USER_ID_INVALID") {
|
||||
t.Fatalf("bad access hash err = %v, want USER_ID_INVALID", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -294,6 +294,7 @@ func (r *Router) registerMessages(d *tg.ServerDispatcher) {
|
|||
}
|
||||
return r.tgMessagesMessages(ctx, userID, r.enrichMessageList(ctx, userID, list)), nil
|
||||
})
|
||||
d.OnMessagesGetRecentLocations(r.onMessagesGetRecentLocations)
|
||||
d.OnMessagesReadHistory(func(ctx context.Context, req *tg.MessagesReadHistoryRequest) (*tg.MessagesAffectedMessages, error) {
|
||||
id, _ := AuthKeyIDFrom(ctx)
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
|
|
|
|||
|
|
@ -716,9 +716,15 @@ func (r *Router) rememberClientAPIID(ctx context.Context, apiID int) {
|
|||
if apiID == 0 {
|
||||
return
|
||||
}
|
||||
info := ClientInfo{APIID: apiID, Type: clientTypeFromAPIID(apiID)}
|
||||
info := normalizeClientInfo(ClientInfo{APIID: apiID, Type: clientTypeFromAPIID(apiID)})
|
||||
sessionInfo := clientSessionInfo{clientInfo: info, hasClientInfo: true}
|
||||
r.rememberClientSessionInfo(ctx, sessionInfo)
|
||||
// api_id is weak evidence. If initConnection (or durable restoration) has
|
||||
// already supplied stronger client metadata, persist that effective session
|
||||
// fact instead of letting auth.sendCode overwrite it on an id collision.
|
||||
if effective, ok, _ := r.clientSessionInfo(ctx); ok {
|
||||
sessionInfo = effective
|
||||
}
|
||||
r.persistAuthKeyClientInfo(ctx, sessionInfo)
|
||||
}
|
||||
|
||||
|
|
@ -1103,7 +1109,7 @@ func clientSessionInfoFromAuthKeyClientInfo(item domain.AuthKeyClientInfo, curre
|
|||
Type: ClientType(item.Platform),
|
||||
},
|
||||
}
|
||||
info.clientInfo = normalizeClientInfo(info.clientInfo)
|
||||
info.clientInfo = restoreClientInfo(info.clientInfo)
|
||||
info.hasClientInfo = info.clientInfo.ClientType() != ClientTypeUnknown ||
|
||||
info.clientInfo.DeviceModel != "" ||
|
||||
info.clientInfo.SystemVersion != "" ||
|
||||
|
|
@ -1162,7 +1168,7 @@ func clientSessionInfoFromAuthorizationRecord(item domain.Authorization, current
|
|||
Type: ClientType(item.Platform),
|
||||
},
|
||||
}
|
||||
info.clientInfo = normalizeClientInfo(info.clientInfo)
|
||||
info.clientInfo = restoreClientInfo(info.clientInfo)
|
||||
info.hasClientInfo = info.clientInfo.ClientType() != ClientTypeUnknown ||
|
||||
info.clientInfo.DeviceModel != "" ||
|
||||
info.clientInfo.SystemVersion != "" ||
|
||||
|
|
|
|||
|
|
@ -281,6 +281,52 @@ func TestDispatchPersistsPreLoginClientMetadataFromSendCodeAPIID(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSendCodeAPIIDDoesNotOverwriteStrongTWebIdentity(t *testing.T) {
|
||||
auth := &captureAuthService{}
|
||||
rawAuthKeyID := [8]byte{0x34, 0xdb, 0xcf, 0xc8, 0x0d, 0x4c, 0x77, 0x97}
|
||||
const sessionID = int64(8103956954238395545)
|
||||
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{Auth: auth}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
initReq := &tg.InvokeWithLayerRequest{
|
||||
Layer: currentClientLayer,
|
||||
Query: &tg.InitConnectionRequest{
|
||||
APIID: 2040,
|
||||
DeviceModel: "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36",
|
||||
SystemVersion: "Win32",
|
||||
AppVersion: "2.2",
|
||||
SystemLangCode: "en-US",
|
||||
LangPack: "webk",
|
||||
LangCode: "en",
|
||||
Query: &tg.HelpGetConfigRequest{},
|
||||
},
|
||||
}
|
||||
var initBuf bin.Buffer
|
||||
if err := initReq.Encode(&initBuf); err != nil {
|
||||
t.Fatalf("encode init request: %v", err)
|
||||
}
|
||||
if _, err := r.Dispatch(context.Background(), rawAuthKeyID, sessionID, &initBuf); err != nil {
|
||||
t.Fatalf("dispatch init request: %v", err)
|
||||
}
|
||||
|
||||
var sendCode bin.Buffer
|
||||
if err := (&tg.AuthSendCodeRequest{
|
||||
PhoneNumber: "+8618800000021",
|
||||
APIID: 2040,
|
||||
APIHash: "tweb-local",
|
||||
Settings: tg.CodeSettings{},
|
||||
}).Encode(&sendCode); err != nil {
|
||||
t.Fatalf("encode auth.sendCode: %v", err)
|
||||
}
|
||||
if _, err := r.Dispatch(context.Background(), rawAuthKeyID, sessionID, &sendCode); err != nil {
|
||||
t.Fatalf("dispatch auth.sendCode: %v", err)
|
||||
}
|
||||
|
||||
persisted := auth.authKeyClientInfos[rawAuthKeyID]
|
||||
if persisted.Platform != string(ClientTypeTWeb) || persisted.DeviceModel != initReq.Query.(*tg.InitConnectionRequest).DeviceModel {
|
||||
t.Fatalf("API id fallback overwrote strong TWeb identity: %+v", persisted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchRestoresPreLoginAndroidMetadataFromAuthKey(t *testing.T) {
|
||||
core, logs := observer.New(zap.DebugLevel)
|
||||
authKeyID := [8]byte{0x22, 0xdb, 0xcf, 0xc8, 0x0d, 0x4c, 0x77, 0x97}
|
||||
|
|
@ -460,33 +506,96 @@ func TestInvokeWithLayerPersistsClientLayerUpgrade(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClientTypeDetectsAndroidSDKVersion(t *testing.T) {
|
||||
info := normalizeClientInfo(ClientInfo{
|
||||
DeviceModel: "GooglePixel 9a",
|
||||
SystemVersion: "SDK 36",
|
||||
AppVersion: "12.7.3 (67509) pbeta",
|
||||
})
|
||||
if got := info.ClientType(); got != ClientTypeAndroid {
|
||||
t.Fatalf("client type = %s, want %s", got, ClientTypeAndroid)
|
||||
func TestClientTypeDetectionUsesStrongEvidenceBeforeAPIID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
info ClientInfo
|
||||
want ClientType
|
||||
}{
|
||||
{
|
||||
name: "iOS 12.8 simulator",
|
||||
info: ClientInfo{APIID: 1, DeviceModel: "iPhone Simulator", SystemVersion: "26.5", AppVersion: "12.8 (10000)", LangPack: "ios"},
|
||||
want: ClientTypeIOS,
|
||||
},
|
||||
{
|
||||
name: "restored iOS without lang pack",
|
||||
info: ClientInfo{DeviceModel: "iPhone 16 Pro", SystemVersion: "18.5", Type: ClientTypeUnknown},
|
||||
want: ClientTypeIOS,
|
||||
},
|
||||
{
|
||||
name: "TWeb WebK",
|
||||
info: ClientInfo{APIID: 1025907, DeviceModel: "Mozilla/5.0 Chrome/138.0", SystemVersion: "Win32", LangPack: "webk"},
|
||||
want: ClientTypeTWeb,
|
||||
},
|
||||
{
|
||||
name: "telegram-tt WebA",
|
||||
info: ClientInfo{APIID: 2040, DeviceModel: "Mozilla/5.0 Chrome/150.0", SystemVersion: "Windows", AppVersion: "12.0.32 A", LangPack: "weba"},
|
||||
want: ClientTypeTelegramTT,
|
||||
},
|
||||
{
|
||||
name: "TWeb borrowed TDesktop API id",
|
||||
info: ClientInfo{APIID: 2040, DeviceModel: "Mozilla/5.0 AppleWebKit/537.36", SystemVersion: "Win32", Type: ClientTypeTDesktop},
|
||||
want: ClientTypeTWeb,
|
||||
},
|
||||
{
|
||||
name: "mobile TWeb is not native Android",
|
||||
info: ClientInfo{APIID: 2040, DeviceModel: "Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36", LangPack: "webk"},
|
||||
want: ClientTypeTWeb,
|
||||
},
|
||||
{
|
||||
name: "Android SDK version",
|
||||
info: ClientInfo{DeviceModel: "GooglePixel 9a", SystemVersion: "SDK 36", AppVersion: "12.7.3 (67509) pbeta"},
|
||||
want: ClientTypeAndroid,
|
||||
},
|
||||
{name: "DrKLO API fallback", info: ClientInfo{APIID: 4}, want: ClientTypeAndroid},
|
||||
{name: "TDesktop API fallback", info: ClientInfo{APIID: 2040}, want: ClientTypeTDesktop},
|
||||
{name: "official iOS API fallback", info: ClientInfo{APIID: 8}, want: ClientTypeIOS},
|
||||
{name: "official TWeb API fallback", info: ClientInfo{APIID: 2496}, want: ClientTypeTWeb},
|
||||
{name: "macOS lang pack", info: ClientInfo{LangPack: "macos"}, want: ClientTypeMacOS},
|
||||
{name: "stored known type", info: ClientInfo{Type: ClientTypeIOS}, want: ClientTypeIOS},
|
||||
{
|
||||
name: "gotd remains unknown",
|
||||
info: ClientInfo{DeviceModel: "go1.26.2", SystemVersion: "windows", AppVersion: "v0.144.0"},
|
||||
want: ClientTypeUnknown,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := normalizeClientInfo(tt.info).ClientType(); got != tt.want {
|
||||
t.Fatalf("client type = %s, want %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
info = normalizeClientInfo(ClientInfo{
|
||||
DeviceModel: "go1.26.2",
|
||||
SystemVersion: "windows",
|
||||
AppVersion: "v0.144.0",
|
||||
func TestRestoredUnknownClientInfoIsNotReclassifiedFromHistoricalFields(t *testing.T) {
|
||||
info := restoreClientInfo(ClientInfo{
|
||||
APIID: 2040,
|
||||
DeviceModel: "Mozilla/5.0 AppleWebKit/537.36",
|
||||
SystemVersion: "Windows",
|
||||
AppVersion: "12.0.32 A",
|
||||
Type: ClientTypeUnknown,
|
||||
})
|
||||
if got := info.ClientType(); got != ClientTypeUnknown {
|
||||
t.Fatalf("gotd test client type = %s, want %s", got, ClientTypeUnknown)
|
||||
t.Fatalf("restored historical client type = %s, want unknown until fresh initConnection", got)
|
||||
}
|
||||
|
||||
info = normalizeClientInfo(ClientInfo{APIID: 4})
|
||||
if got := info.ClientType(); got != ClientTypeAndroid {
|
||||
t.Fatalf("DrKLO api_id=4 client type = %s, want %s", got, ClientTypeAndroid)
|
||||
info = restoreClientInfo(ClientInfo{
|
||||
DeviceModel: "GooglePixel 9a",
|
||||
SystemVersion: "SDK 36",
|
||||
Type: ClientTypeUnknown,
|
||||
})
|
||||
if got := info.ClientType(); got != ClientTypeUnknown {
|
||||
t.Fatalf("restored historical Android client type = %s, want unknown until fresh initConnection", got)
|
||||
}
|
||||
|
||||
info = normalizeClientInfo(ClientInfo{APIID: 2040})
|
||||
if got := info.ClientType(); got != ClientTypeTDesktop {
|
||||
t.Fatalf("TDesktop api_id=2040 client type = %s, want %s", got, ClientTypeTDesktop)
|
||||
info = restoreClientInfo(ClientInfo{
|
||||
DeviceModel: "Mozilla/5.0 AppleWebKit/537.36",
|
||||
AppVersion: "12.0.32 A",
|
||||
Type: ClientTypeTelegramTT,
|
||||
})
|
||||
if got := info.ClientType(); got != ClientTypeTelegramTT {
|
||||
t.Fatalf("restored persisted telegram-tt client type = %s, want %s", got, ClientTypeTelegramTT)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1092,6 +1201,7 @@ func TestTDesktopStartupRPCsEncode(t *testing.T) {
|
|||
{name: "help.getTermsOfServiceUpdate", req: &tg.HelpGetTermsOfServiceUpdateRequest{}},
|
||||
{name: "help.getPremiumPromo", req: &tg.HelpGetPremiumPromoRequest{}},
|
||||
{name: "help.getInviteText", req: &tg.HelpGetInviteTextRequest{}},
|
||||
{name: "help.getAppUpdate", req: &tg.HelpGetAppUpdateRequest{}},
|
||||
{name: "auth.initPasskeyLogin", req: &tg.AuthInitPasskeyLoginRequest{APIID: 4, APIHash: "test"}},
|
||||
{name: "account.getPassword", req: &tg.AccountGetPasswordRequest{}},
|
||||
{name: "account.getNotifySettings", req: &tg.AccountGetNotifySettingsRequest{Peer: &tg.InputNotifyUsers{}}},
|
||||
|
|
@ -1124,6 +1234,7 @@ func TestTDesktopStartupRPCsEncode(t *testing.T) {
|
|||
{name: "account.getSavedRingtones", req: &tg.AccountGetSavedRingtonesRequest{}},
|
||||
{name: "account.resetPassword", req: &tg.AccountResetPasswordRequest{}},
|
||||
{name: "account.updateStatus", req: &tg.AccountUpdateStatusRequest{Offline: true}},
|
||||
{name: "account.updateDeviceLocked", req: &tg.AccountUpdateDeviceLockedRequest{Period: 60}},
|
||||
{name: "payments.getStarsTopupOptions", req: &tg.PaymentsGetStarsTopupOptionsRequest{}},
|
||||
{name: "payments.getStarsStatus", req: &tg.PaymentsGetStarsStatusRequest{Peer: &tg.InputPeerSelf{}}},
|
||||
{name: "updates.getDifference", req: &tg.UpdatesGetDifferenceRequest{}},
|
||||
|
|
@ -1155,6 +1266,7 @@ func TestTDesktopStartupRPCsEncode(t *testing.T) {
|
|||
{name: "messages.getPeerSettings", req: &tg.MessagesGetPeerSettingsRequest{Peer: &tg.InputPeerUser{UserID: domain.OfficialSystemUserID, AccessHash: domain.OfficialSystemUser().AccessHash}}},
|
||||
{name: "messages.setChatWallPaper", req: &tg.MessagesSetChatWallPaperRequest{Peer: &tg.InputPeerUser{UserID: domain.OfficialSystemUserID, AccessHash: domain.OfficialSystemUser().AccessHash}, Wallpaper: &tg.InputWallPaperNoFile{ID: 930000000000000000}}},
|
||||
{name: "messages.getHistory", req: &tg.MessagesGetHistoryRequest{Peer: &tg.InputPeerUser{UserID: domain.OfficialSystemUserID, AccessHash: domain.OfficialSystemUser().AccessHash}, Limit: 20}},
|
||||
{name: "messages.getRecentLocations", req: &tg.MessagesGetRecentLocationsRequest{Peer: &tg.InputPeerUser{UserID: domain.OfficialSystemUserID, AccessHash: domain.OfficialSystemUser().AccessHash}, Limit: 20}},
|
||||
{name: "messages.readHistory", req: &tg.MessagesReadHistoryRequest{Peer: &tg.InputPeerUser{UserID: domain.OfficialSystemUserID, AccessHash: domain.OfficialSystemUser().AccessHash}}},
|
||||
{name: "messages.search", req: &tg.MessagesSearchRequest{Peer: &tg.InputPeerUser{UserID: domain.OfficialSystemUserID, AccessHash: domain.OfficialSystemUser().AccessHash}, Filter: &tg.InputMessagesFilterEmpty{}, Limit: 20}},
|
||||
{name: "messages.searchGlobal", req: &tg.MessagesSearchGlobalRequest{Q: "login", Filter: &tg.InputMessagesFilterEmpty{}, OffsetPeer: &tg.InputPeerEmpty{}, Limit: 20}},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package rpc
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"hash/fnv"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
"go.uber.org/zap"
|
||||
|
|
@ -24,15 +26,16 @@ func (r *Router) onMessagesGetAvailableReactions(ctx context.Context, hash int)
|
|||
if len(reactions) == 0 {
|
||||
return tdesktop.AvailableReactions(hash), nil
|
||||
}
|
||||
catalogHash := availableReactionsHash(reactions)
|
||||
if hash == catalogHash {
|
||||
return &tg.MessagesAvailableReactionsNotModified{}, nil
|
||||
}
|
||||
docs, err := r.deps.Files.GetDocuments(ctx, reactionDocumentIDs(reactions))
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
return tgAvailableReactions(reactions, documentsByID(docs), catalogHash), nil
|
||||
docByID := documentsByID(docs)
|
||||
catalogHash := availableReactionsHash(reactions, docByID)
|
||||
if hash == catalogHash {
|
||||
return &tg.MessagesAvailableReactionsNotModified{}, nil
|
||||
}
|
||||
return tgAvailableReactions(reactions, docByID, catalogHash), nil
|
||||
}
|
||||
|
||||
// onMessagesGetAvailableEffects 返回消息发送特效目录(全局静态,seed 进内存)。镜像
|
||||
|
|
@ -457,24 +460,34 @@ func documentsByID(docs []domain.Document) map[int64]domain.Document {
|
|||
return m
|
||||
}
|
||||
|
||||
// availableReactionsHash 用 reaction 的核心字段算稳定 hash(供 *NotModified 缓存判定)。
|
||||
func availableReactionsHash(reactions []domain.AvailableReaction) int {
|
||||
values := make([]int64, 0, len(reactions)*10)
|
||||
// availableReactionsHash covers every domain field that contributes to the TL
|
||||
// response, including the embedded documents. A repaired file reference,
|
||||
// attribute, thumbnail, title, or emoji must invalidate clients which cached an
|
||||
// older response; hashing only document ids leaves those clients permanently on
|
||||
// stale resources after a seed repair.
|
||||
func availableReactionsHash(reactions []domain.AvailableReaction, docByID map[int64]domain.Document) int {
|
||||
h := fnv.New32a()
|
||||
for _, r := range reactions {
|
||||
values = append(values,
|
||||
int64(len([]rune(r.Reaction))),
|
||||
boolHashValue(r.Inactive),
|
||||
boolHashValue(r.Premium),
|
||||
r.StaticIconID,
|
||||
r.AppearAnimationID,
|
||||
r.SelectAnimationID,
|
||||
r.ActivateAnimationID,
|
||||
r.EffectAnimationID,
|
||||
r.AroundAnimationID,
|
||||
r.CenterIconID,
|
||||
)
|
||||
encoded, _ := json.Marshal(r)
|
||||
_, _ = h.Write(encoded)
|
||||
_, _ = h.Write([]byte{0xff})
|
||||
for _, id := range r.DocumentIDs() {
|
||||
doc, ok := docByID[id]
|
||||
if !ok {
|
||||
// Missing mandatory/optional documents are part of the response as
|
||||
// documentEmpty{id}; keep that state hashable as well.
|
||||
doc.ID = id
|
||||
}
|
||||
encoded, _ = json.Marshal(doc)
|
||||
_, _ = h.Write(encoded)
|
||||
_, _ = h.Write([]byte{0xfe})
|
||||
}
|
||||
}
|
||||
return int(tdesktopCountHash(values) & 0x7fffffff)
|
||||
sum := int(h.Sum32() & 0x7fffffff)
|
||||
if sum == 0 {
|
||||
return 1
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func stickerSetsCatalogHash(sets []domain.StickerSet) int64 {
|
||||
|
|
|
|||
|
|
@ -565,6 +565,42 @@ func TestMessagesGetAvailableReactionsNotModified(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMessagesGetAvailableReactionsDocumentRepairInvalidatesHash(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
reactions := []domain.AvailableReaction{{
|
||||
Reaction: "❤", Title: "Heart", StaticIconID: 101, AppearAnimationID: 102,
|
||||
SelectAnimationID: 103, ActivateAnimationID: 104, EffectAnimationID: 105,
|
||||
}}
|
||||
docs := map[int64]domain.Document{}
|
||||
for _, id := range reactions[0].DocumentIDs() {
|
||||
docs[id] = domain.Document{ID: id, AccessHash: id + 1000, DCID: 2, MimeType: "application/x-tgsticker", Size: 10}
|
||||
}
|
||||
files := &fakeFiles{reactions: reactions, docs: docs}
|
||||
r := &Router{deps: Deps{Files: files}}
|
||||
|
||||
first, err := r.onMessagesGetAvailableReactions(ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("first getAvailableReactions: %v", err)
|
||||
}
|
||||
oldHash := first.(*tg.MessagesAvailableReactions).Hash
|
||||
|
||||
repaired := docs[103]
|
||||
repaired.FileReference = []byte("repaired-reference")
|
||||
repaired.Size = 11
|
||||
docs[103] = repaired
|
||||
second, err := r.onMessagesGetAvailableReactions(ctx, oldHash)
|
||||
if err != nil {
|
||||
t.Fatalf("getAvailableReactions after repair: %v", err)
|
||||
}
|
||||
full, ok := second.(*tg.MessagesAvailableReactions)
|
||||
if !ok {
|
||||
t.Fatalf("after document repair = %T, want full response", second)
|
||||
}
|
||||
if full.Hash == oldHash {
|
||||
t.Fatalf("document repair kept hash %d", oldHash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTGDocumentCompactsCachedThumbToDownloadableSize(t *testing.T) {
|
||||
doc := tgDocument(domain.Document{
|
||||
ID: 100,
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ func (r *Router) registerUpdates(d *tg.ServerDispatcher) {
|
|||
|
||||
// onUpdatesGetState 处理 updates.getState。TDesktop 与 DrKLO 的启动路径把它当作
|
||||
// 「从当前快照开始同步」的显式 baseline:返回账号当前连续水位并推进该设备 observed。
|
||||
// 对无法识别的客户端仍返回同一 current state,但不把尚未被客户端带回的服务端快照
|
||||
// 记成 observed;这保留 durable difference tail,避免把 TDesktop/DrKLO 的兼容例外
|
||||
// 扩散成所有客户端都能跨过未实际确认事件的 retention 后门。
|
||||
// 对尚未审计 baseline 语义的客户端仍返回同一 current state,但不把尚未被客户端带回
|
||||
// 的服务端快照记成 observed;这保留 durable difference tail,避免把 TDesktop/DrKLO
|
||||
// 的兼容例外扩散成所有客户端都能跨过未实际确认事件的 retention 后门。
|
||||
func (r *Router) onUpdatesGetState(ctx context.Context) (*tg.UpdatesState, error) {
|
||||
id, _ := AuthKeyIDFrom(ctx)
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
|
|
@ -35,7 +35,7 @@ func (r *Router) onUpdatesGetState(ctx context.Context) (*tg.UpdatesState, error
|
|||
} else {
|
||||
st, err = r.deps.Updates.CurrentState(ctx, userID)
|
||||
if err == nil {
|
||||
r.log.Warn("updates.getState returned current snapshot without advancing observed baseline for unknown client",
|
||||
r.log.Warn("updates.getState returned current snapshot without advancing observed baseline for client without audited baseline policy",
|
||||
r.contextLogFields(ctx)...)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue