From 76bfc5100f831ee304a876251f8fee99171fed33 Mon Sep 17 00:00:00 2001 From: A Date: Wed, 15 Jul 2026 16:07:43 +0800 Subject: [PATCH] fix: sync WebK compatibility paths --- internal/compat/tdesktop/config.go | 4 +- internal/compat/tdesktop/config_test.go | 20 +++ internal/compat/tdesktop/startup_stubs.go | 8 +- internal/rpc/account.go | 2 +- internal/rpc/client_info_test.go | 106 ++++++++++++++ internal/rpc/context.go | 28 ++++ internal/rpc/help.go | 27 +++- internal/rpc/messages_contracts.go | 6 +- internal/rpc/messages_reactions_privacy.go | 35 +++++ internal/rpc/messages_received.go | 25 ++++ internal/rpc/messages_received_config_test.go | 133 ++++++++++++++++++ internal/rpc/messages_register.go | 1 + 12 files changed, 387 insertions(+), 8 deletions(-) create mode 100644 internal/compat/tdesktop/config_test.go create mode 100644 internal/rpc/client_info_test.go create mode 100644 internal/rpc/messages_received.go create mode 100644 internal/rpc/messages_received_config_test.go diff --git a/internal/compat/tdesktop/config.go b/internal/compat/tdesktop/config.go index f4de71e4..e551cf3b 100644 --- a/internal/compat/tdesktop/config.go +++ b/internal/compat/tdesktop/config.go @@ -14,7 +14,7 @@ import ( // (记录于 docs/compatibility-matrix.md)。 func BuildConfig(dc int, ip string, port int, now time.Time, publicBaseURL string) *tg.Config { meURLPrefix := links.NormalizeBaseURL(publicBaseURL) + "/" - return &tg.Config{ + config := &tg.Config{ Date: int(now.Unix()), Expires: int(now.Add(time.Hour).Unix()), TestMode: false, @@ -55,6 +55,8 @@ func BuildConfig(dc int, ip string, port int, now time.Time, publicBaseURL strin MessageLengthMax: 4096, WebfileDCID: dc, } + config.SetReactionsDefault(&tg.ReactionEmoji{Emoticon: DefaultReactionEmoticon}) + return config } // NearestDC 构造 help.getNearestDc 返回值。 diff --git a/internal/compat/tdesktop/config_test.go b/internal/compat/tdesktop/config_test.go new file mode 100644 index 00000000..61b81953 --- /dev/null +++ b/internal/compat/tdesktop/config_test.go @@ -0,0 +1,20 @@ +package tdesktop + +import ( + "testing" + "time" + + "github.com/iamxvbaba/td/tg" +) + +func TestBuildConfigIncludesDefaultReaction(t *testing.T) { + config := BuildConfig(2, "127.0.0.1", 2398, time.Unix(1, 0), "https://telesrv.net") + reaction, ok := config.GetReactionsDefault() + if !ok { + t.Fatal("reactions_default is absent") + } + emoji, ok := reaction.(*tg.ReactionEmoji) + if !ok || emoji.Emoticon != DefaultReactionEmoticon { + t.Fatalf("reactions_default = %#v, want %q emoji", reaction, DefaultReactionEmoticon) + } +} diff --git a/internal/compat/tdesktop/startup_stubs.go b/internal/compat/tdesktop/startup_stubs.go index 1c992687..e6dab967 100644 --- a/internal/compat/tdesktop/startup_stubs.go +++ b/internal/compat/tdesktop/startup_stubs.go @@ -194,7 +194,7 @@ func UniqueGiftChatThemes(hash int64) tg.AccountChatThemesClass { } } -// WallPapers returns the read-only Default wallpaper catalog. User wallpaper +// WallPapers returns the read-only default wallpaper catalog. User wallpaper // upload/save/install remains outside the current TDesktop compatibility scope. func WallPapers(hash int64) tg.AccountWallPapersClass { if hash == wallPapersHash { @@ -256,13 +256,17 @@ func DefaultGroupPhotoEmojis() tg.EmojiListClass { const availableReactionsHash = 20260602 const emptyStickerSetHash = 20260602 +// DefaultReactionEmoticon is the account fallback used by config and the +// built-in available-reactions catalog. +const DefaultReactionEmoticon = "\U0001f44d" + type defaultReaction struct { emoticon string title string } var defaultAvailableReactions = []defaultReaction{ - {emoticon: "\U0001f44d", title: "Thumbs Up"}, + {emoticon: DefaultReactionEmoticon, title: "Thumbs Up"}, {emoticon: "\u2764\ufe0f", title: "Red Heart"}, {emoticon: "\U0001f602", title: "Face With Tears of Joy"}, {emoticon: "\U0001f62e", title: "Face With Open Mouth"}, diff --git a/internal/rpc/account.go b/internal/rpc/account.go index 45872759..0f84c3b1 100644 --- a/internal/rpc/account.go +++ b/internal/rpc/account.go @@ -1100,7 +1100,7 @@ func privacyErr(err error) error { } type accountReactionSettingsService interface { - GetReactionSettings(ctx context.Context, userID int64) (domain.AccountReactionSettings, error) + accountReactionSettingsReader SetReactionsNotifySettings(ctx context.Context, userID int64, settings domain.ReactionsNotifySettings) (domain.AccountReactionSettings, error) } diff --git a/internal/rpc/client_info_test.go b/internal/rpc/client_info_test.go new file mode 100644 index 00000000..d49eebf9 --- /dev/null +++ b/internal/rpc/client_info_test.go @@ -0,0 +1,106 @@ +package rpc + +import ( + "context" + "strings" + "testing" + "unicode/utf8" +) + +func TestNormalizeClientInfoBoundsMetadataAfterClassification(t *testing.T) { + raw := ClientInfo{ + APIID: 2040, + DeviceModel: "Mozilla/5.0 (iPhone) AppleWebKit/605.1.15 Safari/604.1 " + strings.Repeat("x", 160), + SystemVersion: strings.Repeat("iOS", 30), + AppVersion: strings.Repeat("2.2", 30), + SystemLangCode: strings.Repeat("zh-", 30), + LangPack: "webk" + strings.Repeat("x", 80), + LangCode: strings.Repeat("zh-", 30), + } + + got := normalizeClientInfo(raw) + if got.ClientType() != ClientTypeTWeb { + t.Fatalf("client type = %s, want %s", got.ClientType(), ClientTypeTWeb) + } + assertClientMetadataRunes(t, "device model", got.DeviceModel, maxClientDeviceModelRunes) + assertClientMetadataRunes(t, "system version", got.SystemVersion, maxClientMetadataRunes) + assertClientMetadataRunes(t, "app version", got.AppVersion, maxClientMetadataRunes) + assertClientMetadataRunes(t, "system language", got.SystemLangCode, maxClientMetadataRunes) + assertClientMetadataRunes(t, "language pack", got.LangPack, maxClientMetadataRunes) + assertClientMetadataRunes(t, "language code", got.LangCode, maxClientMetadataRunes) +} + +func TestNormalizeClientInfoUsesRuneLimitsAndProducesValidUTF8(t *testing.T) { + got := normalizeClientInfo(ClientInfo{ + DeviceModel: strings.Repeat("\u754c", maxClientDeviceModelRunes+1), + SystemVersion: strings.Repeat("\U0001f4f1", maxClientMetadataRunes+1), + AppVersion: "2.2" + string([]byte{0xff}), + LangPack: "webk", + }) + + assertClientMetadataRunes(t, "device model", got.DeviceModel, maxClientDeviceModelRunes) + assertClientMetadataRunes(t, "system version", got.SystemVersion, maxClientMetadataRunes) + if !utf8.ValidString(got.AppVersion) { + t.Fatalf("app version is not valid UTF-8: %q", got.AppVersion) + } +} + +func TestNormalizedClientInfoIsSharedByAuthPersistencePaths(t *testing.T) { + authKeyID := [8]byte{1, 2, 3, 4, 5, 6, 7, 8} + ctx := WithLayer(context.Background(), 227) + ctx = WithAuthKeyID(ctx, authKeyID) + ctx = WithClientInfo(ctx, ClientInfo{ + APIID: 2040, + DeviceModel: "Mozilla/5.0 AppleWebKit/605.1.15 " + strings.Repeat("x", 160), + SystemVersion: strings.Repeat("iOS", 30), + AppVersion: strings.Repeat("2.2", 30), + LangPack: "webk", + }) + info, ok := ClientInfoFrom(ctx) + if !ok { + t.Fatal("normalized client info missing from context") + } + + authz := (&Router{}).authzFromCtx(ctx) + authKeyInfo := domainAuthKeyClientInfo(clientSessionInfo{ + layer: LayerFrom(ctx), + hasClientInfo: true, + clientInfo: info, + }) + if authz.DeviceModel != authKeyInfo.DeviceModel || + authz.SystemVersion != authKeyInfo.SystemVersion || + authz.AppVersion != authKeyInfo.AppVersion || + authz.Platform != authKeyInfo.Platform || + authz.APIID != authKeyInfo.APIID || + authz.Layer != authKeyInfo.Layer { + t.Fatalf("authorization metadata %+v differs from auth-key metadata %+v", authz, authKeyInfo) + } +} + +func TestNormalizeClientInfoPreservesMetadataWithinLimits(t *testing.T) { + raw := ClientInfo{ + APIID: 4, + DeviceModel: "Google Pixel 9", + SystemVersion: "SDK 36", + AppVersion: "12.7.3", + SystemLangCode: "en-US", + LangPack: "android", + LangCode: "en", + } + got := normalizeClientInfo(raw) + if got.DeviceModel != raw.DeviceModel || got.SystemVersion != raw.SystemVersion || + got.AppVersion != raw.AppVersion || got.SystemLangCode != raw.SystemLangCode || + got.LangPack != raw.LangPack || got.LangCode != raw.LangCode { + t.Fatalf("metadata within limits changed: got %+v, want %+v", got, raw) + } +} + +func assertClientMetadataRunes(t *testing.T, field, value string, want int) { + t.Helper() + if got := utf8.RuneCountInString(value); got != want { + t.Fatalf("%s rune count = %d, want %d", field, got, want) + } + if !utf8.ValidString(value) { + t.Fatalf("%s is not valid UTF-8", field) + } +} diff --git a/internal/rpc/context.go b/internal/rpc/context.go index 69e1e40c..f79a7180 100644 --- a/internal/rpc/context.go +++ b/internal/rpc/context.go @@ -4,6 +4,7 @@ import ( "context" "regexp" "strings" + "unicode/utf8" "github.com/iamxvbaba/td/tg" ) @@ -35,6 +36,11 @@ func inboundRPCBytesFrom(ctx context.Context) int { const currentClientLayer = tg.Layer +const ( + maxClientDeviceModelRunes = 128 + maxClientMetadataRunes = 64 +) + var androidSDKVersionRE = regexp.MustCompile(`\bsdk\s+\d+\b`) type ClientType string @@ -98,11 +104,33 @@ func ClientTypeFrom(ctx context.Context) ClientType { } func normalizeClientInfo(info ClientInfo) ClientInfo { + // Classify against the complete wire metadata before bounding the values + // shared by auth-key and authorization persistence. info.Type = detectClientType(info) + info.DeviceModel = truncateClientMetadata(info.DeviceModel, maxClientDeviceModelRunes) + info.SystemVersion = truncateClientMetadata(info.SystemVersion, maxClientMetadataRunes) + info.AppVersion = truncateClientMetadata(info.AppVersion, maxClientMetadataRunes) + info.SystemLangCode = truncateClientMetadata(info.SystemLangCode, maxClientMetadataRunes) + info.LangPack = truncateClientMetadata(info.LangPack, maxClientMetadataRunes) + info.LangCode = truncateClientMetadata(info.LangCode, maxClientMetadataRunes) info.typeResolved = true return info } +func truncateClientMetadata(value string, maxRunes int) string { + if maxRunes <= 0 || value == "" { + return "" + } + if utf8.ValidString(value) && utf8.RuneCountInString(value) <= maxRunes { + return value + } + runes := []rune(value) + if len(runes) > maxRunes { + runes = runes[:maxRunes] + } + return string(runes) +} + func (info ClientInfo) ClientType() ClientType { if info.typeResolved { if knownClientType(info.Type) { diff --git a/internal/rpc/help.go b/internal/rpc/help.go index 428672e0..08362658 100644 --- a/internal/rpc/help.go +++ b/internal/rpc/help.go @@ -13,9 +13,7 @@ import ( // registerHelp 注册 help.* RPC handler(DC 配置、最近 DC)。 func (r *Router) registerHelp(d *tg.ServerDispatcher) { - d.OnHelpGetConfig(func(ctx context.Context) (*tg.Config, error) { - return tdesktop.BuildConfig(r.cfg.DC, r.cfg.IP, r.cfg.Port, r.clock.Now(), r.cfg.PublicBaseURL), nil - }) + d.OnHelpGetConfig(r.onHelpGetConfig) d.OnHelpGetNearestDC(func(ctx context.Context) (*tg.NearestDC, error) { return tdesktop.NearestDC(r.cfg.DC), nil }) @@ -81,6 +79,29 @@ func (r *Router) registerHelp(d *tg.ServerDispatcher) { d.OnHelpGetPremiumPromo(r.onHelpGetPremiumPromo) } +func (r *Router) onHelpGetConfig(ctx context.Context) (*tg.Config, error) { + config := tdesktop.BuildConfig(r.cfg.DC, r.cfg.IP, r.cfg.Port, r.clock.Now(), r.cfg.PublicBaseURL) + userID, authorized, err := r.currentUserID(ctx) + if err != nil { + return nil, internalErr() + } + if !authorized || userID == 0 { + return config, nil + } + if svc, ok := r.deps.Account.(accountReactionSettingsReader); ok { + settings, err := svc.GetReactionSettings(ctx, userID) + if err != nil { + return nil, internalErr() + } + reaction := tgMessageReaction(settings.DefaultReaction) + if reaction == nil { + return nil, internalErr() + } + config.SetReactionsDefault(reaction) + } + return config, nil +} + // onHelpDismissSuggestion 为 DrKLO 改号成功后的 suggestion 清理提供有界兼容。 // Android 会先把 suggestion 从本地状态删除,再发送该 RPC,且 generic 500 会被 // 连接层持续重试。当前 server 不发布 pending suggestions,故非空 dismissal diff --git a/internal/rpc/messages_contracts.go b/internal/rpc/messages_contracts.go index 13478fa2..22ccbefd 100644 --- a/internal/rpc/messages_contracts.go +++ b/internal/rpc/messages_contracts.go @@ -10,8 +10,12 @@ type accountDefaultReactionService interface { SetDefaultReaction(ctx context.Context, userID int64, reaction domain.MessageReaction) (domain.AccountReactionSettings, error) } -type accountPaidReactionPrivacyService interface { +type accountReactionSettingsReader interface { GetReactionSettings(ctx context.Context, userID int64) (domain.AccountReactionSettings, error) +} + +type accountPaidReactionPrivacyService interface { + accountReactionSettingsReader SetPaidReactionPrivacy(ctx context.Context, userID int64, privacy domain.PaidReactionPrivacy) (domain.AccountReactionSettings, error) } diff --git a/internal/rpc/messages_reactions_privacy.go b/internal/rpc/messages_reactions_privacy.go index 3215aba9..2e8193a0 100644 --- a/internal/rpc/messages_reactions_privacy.go +++ b/internal/rpc/messages_reactions_privacy.go @@ -2,6 +2,7 @@ package rpc import ( "context" + "strings" "github.com/iamxvbaba/td/tg" "go.uber.org/zap" @@ -18,6 +19,9 @@ func (r *Router) onMessagesSetDefaultReaction(ctx context.Context, reaction tg.R if err != nil { return false, err } + if err := r.validateDefaultReaction(ctx, parsed); err != nil { + return false, err + } if svc, ok := r.deps.Account.(accountDefaultReactionService); ok { if _, err := svc.SetDefaultReaction(ctx, userID, parsed); err != nil { return false, internalErr() @@ -26,6 +30,37 @@ func (r *Router) onMessagesSetDefaultReaction(ctx context.Context, reaction tg.R return true, nil } +func (r *Router) validateDefaultReaction(ctx context.Context, reaction domain.MessageReaction) error { + if reaction.Type == domain.MessageReactionCustomEmoji { + return nil + } + if reaction.Type != domain.MessageReactionEmoji { + return reactionInvalidErr() + } + + if r.deps.Files != nil { + catalog, err := r.deps.Files.ListAvailableReactions(ctx) + if err != nil { + return internalErr() + } + if len(catalog) > 0 { + for _, item := range catalog { + if !item.Inactive && strings.TrimSpace(item.Reaction) == reaction.Emoticon { + return nil + } + } + return reactionInvalidErr() + } + } + + for _, item := range staticReactionCatalog() { + if item.Key() == reaction.Key() { + return nil + } + } + return reactionInvalidErr() +} + func (r *Router) onMessagesGetPaidReactionPrivacy(ctx context.Context) (tg.UpdatesClass, error) { userID, _, err := r.currentUserID(ctx) if err != nil { diff --git a/internal/rpc/messages_received.go b/internal/rpc/messages_received.go new file mode 100644 index 00000000..b0be9402 --- /dev/null +++ b/internal/rpc/messages_received.go @@ -0,0 +1,25 @@ +package rpc + +import ( + "context" + + "github.com/iamxvbaba/td/tg" +) + +// onMessagesReceivedMessages acknowledges the client's highest observed +// message id. telesrv does not enqueue mobile PUSH notifications, so there are +// no pending notifications to cancel and the exact cancellation set is empty. +// This acknowledgement must never advance read boundaries or update state. +func (r *Router) onMessagesReceivedMessages(ctx context.Context, _ int) ([]tg.ReceivedNotifyMessage, error) { + userID, authorized, err := r.currentUserID(ctx) + if err != nil { + return nil, internalErr() + } + if !authorized || userID == 0 { + return nil, authKeyUnregisteredErr() + } + if r.userIsBot(ctx, userID) { + return nil, botMethodInvalidErr() + } + return []tg.ReceivedNotifyMessage{}, nil +} diff --git a/internal/rpc/messages_received_config_test.go b/internal/rpc/messages_received_config_test.go new file mode 100644 index 00000000..6cb9750b --- /dev/null +++ b/internal/rpc/messages_received_config_test.go @@ -0,0 +1,133 @@ +package rpc + +import ( + "context" + "errors" + "testing" + + "github.com/iamxvbaba/td/bin" + "github.com/iamxvbaba/td/clock" + "github.com/iamxvbaba/td/tg" + "github.com/iamxvbaba/td/tgerr" + "go.uber.org/zap/zaptest" + + "telesrv/internal/domain" +) + +type reactionSettingsAccountStub struct { + AccountService + settings domain.AccountReactionSettings + getErr error + setCalls int +} + +func (s *reactionSettingsAccountStub) GetReactionSettings(context.Context, int64) (domain.AccountReactionSettings, error) { + return s.settings, s.getErr +} + +func (s *reactionSettingsAccountStub) SetDefaultReaction(_ context.Context, _ int64, reaction domain.MessageReaction) (domain.AccountReactionSettings, error) { + s.setCalls++ + s.settings.DefaultReaction = reaction + return s.settings, nil +} + +type reactionCatalogErrorFiles struct { + FilesService +} + +func (reactionCatalogErrorFiles) ListAvailableReactions(context.Context) ([]domain.AvailableReaction, error) { + return nil, errors.New("catalog unavailable") +} + +func TestHelpGetConfigReturnsDefaultAndAccountReaction(t *testing.T) { + account := &reactionSettingsAccountStub{settings: domain.DefaultAccountReactionSettings()} + r := New(Config{DC: 2, PublicBaseURL: "https://telesrv.net"}, Deps{Account: account}, zaptest.NewLogger(t), clock.System) + + preAuth, err := r.onHelpGetConfig(context.Background()) + if err != nil { + t.Fatalf("pre-auth help.getConfig: %v", err) + } + if emoji, ok := preAuth.ReactionsDefault.(*tg.ReactionEmoji); !ok || emoji.Emoticon != "\U0001f44d" { + t.Fatalf("pre-auth reactions_default = %#v, want thumbs up", preAuth.ReactionsDefault) + } + + account.settings.DefaultReaction = domain.MessageReaction{Type: domain.MessageReactionCustomEmoji, DocumentID: 7001} + authorized, err := r.onHelpGetConfig(WithUserID(context.Background(), 42)) + if err != nil { + t.Fatalf("authorized help.getConfig: %v", err) + } + if custom, ok := authorized.ReactionsDefault.(*tg.ReactionCustomEmoji); !ok || custom.DocumentID != 7001 { + t.Fatalf("authorized reactions_default = %#v, want custom emoji 7001", authorized.ReactionsDefault) + } +} + +func TestSetDefaultReactionRequiresActiveCatalogEmoji(t *testing.T) { + account := &reactionSettingsAccountStub{settings: domain.DefaultAccountReactionSettings()} + files := &fakeFiles{reactions: []domain.AvailableReaction{ + {Reaction: "\U0001f525"}, + {Reaction: "\U0001f602", Inactive: true}, + }} + r := New(Config{}, Deps{Account: account, Files: files}, zaptest.NewLogger(t), clock.System) + ctx := WithUserID(context.Background(), 42) + + if ok, err := r.onMessagesSetDefaultReaction(ctx, &tg.ReactionEmoji{Emoticon: "\U0001f525"}); err != nil || !ok { + t.Fatalf("set active reaction = %v, %v", ok, err) + } + if account.setCalls != 1 { + t.Fatalf("set calls = %d, want 1", account.setCalls) + } + for _, emoticon := range []string{"\U0001f602", "\U0001f680"} { + if _, err := r.onMessagesSetDefaultReaction(ctx, &tg.ReactionEmoji{Emoticon: emoticon}); !tgerr.Is(err, "REACTION_INVALID") { + t.Fatalf("set reaction %q err = %v, want REACTION_INVALID", emoticon, err) + } + } + if account.setCalls != 1 { + t.Fatalf("invalid reactions reached persistence: set calls = %d", account.setCalls) + } +} + +func TestSetDefaultReactionFailsClosedWhenCatalogReadFails(t *testing.T) { + account := &reactionSettingsAccountStub{settings: domain.DefaultAccountReactionSettings()} + r := New(Config{}, Deps{Account: account, Files: reactionCatalogErrorFiles{}}, zaptest.NewLogger(t), clock.System) + if _, err := r.onMessagesSetDefaultReaction(WithUserID(context.Background(), 42), &tg.ReactionEmoji{Emoticon: "\U0001f44d"}); !tgerr.Is(err, "INTERNAL_SERVER_ERROR") { + t.Fatalf("set reaction err = %v, want INTERNAL_SERVER_ERROR", err) + } + if account.setCalls != 0 { + t.Fatalf("failed catalog read reached persistence: set calls = %d", account.setCalls) + } +} + +func TestMessagesReceivedMessagesIsAuthorizedNoop(t *testing.T) { + r := New(Config{}, Deps{}, zaptest.NewLogger(t), clock.System) + for _, maxID := range []int{-1, 0, 1, int(^uint32(0) >> 1)} { + got, err := r.onMessagesReceivedMessages(WithUserID(context.Background(), 42), maxID) + if err != nil { + t.Fatalf("max_id %d: %v", maxID, err) + } + if got == nil || len(got) != 0 { + t.Fatalf("max_id %d result = %#v, want non-nil empty cancellation set", maxID, got) + } + } + if _, err := r.onMessagesReceivedMessages(context.Background(), 1); !tgerr.Is(err, "AUTH_KEY_UNREGISTERED") { + t.Fatalf("unauthorized err = %v, want AUTH_KEY_UNREGISTERED", err) + } +} + +func TestMessagesReceivedMessagesIsRegistered(t *testing.T) { + r := New(Config{}, Deps{}, zaptest.NewLogger(t), clock.System) + var request bin.Buffer + if err := (&tg.MessagesReceivedMessagesRequest{MaxID: 99}).Encode(&request); err != nil { + t.Fatalf("encode messages.receivedMessages: %v", err) + } + result, method, err := r.DispatchWithMethod(WithUserID(context.Background(), 42), [8]byte{1}, 7, &request) + if err != nil { + t.Fatalf("dispatch messages.receivedMessages: %v", err) + } + if method != "messages.receivedMessages" { + t.Fatalf("method = %q, want messages.receivedMessages", method) + } + vector, ok := result.(*tg.ReceivedNotifyMessageVector) + if !ok || vector == nil || vector.Elems == nil || len(vector.Elems) != 0 { + t.Fatalf("result = %#v (%T), want non-nil empty ReceivedNotifyMessageVector", result, result) + } +} diff --git a/internal/rpc/messages_register.go b/internal/rpc/messages_register.go index cd8af767..dfeb792a 100644 --- a/internal/rpc/messages_register.go +++ b/internal/rpc/messages_register.go @@ -10,6 +10,7 @@ import ( // registerMessages 注册 messages.* RPC handler。 func (r *Router) registerMessages(d *tg.ServerDispatcher) { + d.OnMessagesReceivedMessages(r.onMessagesReceivedMessages) d.OnMessagesSetTyping(r.onMessagesSetTyping) d.OnMessagesSaveDraft(r.onMessagesSaveDraft) d.OnMessagesSaveDefaultSendAs(r.onMessagesSaveDefaultSendAs)