From 840ef6237cce67a6fb3c301d0a0f717a6e9b2094 Mon Sep 17 00:00:00 2001 From: iamxvbaba <28732408+iamxvbaba@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:57:01 +0800 Subject: [PATCH] fix: sync DrKLO report selection support --- internal/compat/android/report.go | 22 +++++++++++++ internal/compat/android/report_test.go | 32 +++++++++++++++++++ .../rpc/channels_passive_stubs_rpc_test.go | 30 +++++++++++++++++ internal/rpc/messages_reports.go | 9 ++++++ 4 files changed, 93 insertions(+) create mode 100644 internal/compat/android/report.go create mode 100644 internal/compat/android/report_test.go diff --git a/internal/compat/android/report.go b/internal/compat/android/report.go new file mode 100644 index 00000000..d4c60e30 --- /dev/null +++ b/internal/compat/android/report.go @@ -0,0 +1,22 @@ +package android + +const clientType = "android" + +// OfferInitialMessageReportOptions preserves DrKLO's official channel-report +// flow. DrKLO starts that flow with an empty message-id vector and only opens +// its message selector after a chosen option receives MESSAGE_ID_REQUIRED. +// +// This exception is deliberately limited to the non-mutating first request: +// a selected option, a comment, or any non-Android caller must still pass the +// normal messages.report message-id validation. +func OfferInitialMessageReportOptions( + client string, + messageIDCount int, + option []byte, + comment string, +) bool { + return client == clientType && + messageIDCount == 0 && + len(option) == 0 && + comment == "" +} diff --git a/internal/compat/android/report_test.go b/internal/compat/android/report_test.go new file mode 100644 index 00000000..2ac16eae --- /dev/null +++ b/internal/compat/android/report_test.go @@ -0,0 +1,32 @@ +package android + +import "testing" + +func TestOfferInitialMessageReportOptions(t *testing.T) { + tests := []struct { + name string + client string + messageIDCount int + option []byte + comment string + want bool + }{ + {name: "android initial discovery", client: "android", want: true}, + {name: "desktop keeps protocol error", client: "tdesktop"}, + {name: "selected option requires messages", client: "android", option: []byte("spam")}, + {name: "comment requires messages", client: "android", comment: "details"}, + {name: "message ids use normal flow", client: "android", messageIDCount: 1}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := OfferInitialMessageReportOptions( + test.client, + test.messageIDCount, + test.option, + test.comment, + ); got != test.want { + t.Fatalf("OfferInitialMessageReportOptions() = %v, want %v", got, test.want) + } + }) + } +} diff --git a/internal/rpc/channels_passive_stubs_rpc_test.go b/internal/rpc/channels_passive_stubs_rpc_test.go index d39598a4..77214d85 100644 --- a/internal/rpc/channels_passive_stubs_rpc_test.go +++ b/internal/rpc/channels_passive_stubs_rpc_test.go @@ -449,6 +449,36 @@ func TestTDesktopPassiveChannelStubs(t *testing.T) { if ok, err := r.onMessagesReportSpam(ownerCtx, inputPeerChannel(channel)); err != nil || !ok { t.Fatalf("messages.reportSpam = ok %v err %v, want true nil", ok, err) } + if _, err := r.onMessagesReport(ownerCtx, &tg.MessagesReportRequest{ + Peer: inputPeerChannel(channel), + }); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_REQUIRED") { + t.Fatalf("desktop messages.report without ids err = %v, want MESSAGE_ID_REQUIRED", err) + } + androidReportOptions, err := r.onMessagesReport( + WithClientInfo(ownerCtx, ClientInfo{ + Type: ClientTypeAndroid, + AppVersion: "12.9.0 (69669) pbeta", + }), + &tg.MessagesReportRequest{Peer: inputPeerChannel(channel)}, + ) + if err != nil { + t.Fatalf("android messages.report initial options: %v", err) + } + if choices, ok := androidReportOptions.(*tg.ReportResultChooseOption); !ok || len(choices.Options) == 0 { + t.Fatalf("android messages.report initial options = %#v, want chooseOption", androidReportOptions) + } + if got := moderationReports.Reports(); len(got) != 1 { + t.Fatalf("android initial report option discovery persisted reports = %+v, want only earlier reportSpam", got) + } + if _, err := r.onMessagesReport( + WithClientInfo(ownerCtx, ClientInfo{Type: ClientTypeAndroid}), + &tg.MessagesReportRequest{ + Peer: inputPeerChannel(channel), + Option: []byte("spam"), + }, + ); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_REQUIRED") { + t.Fatalf("android selected report option without ids err = %v, want MESSAGE_ID_REQUIRED", err) + } reportOptions, err := r.onMessagesReport(ownerCtx, &tg.MessagesReportRequest{ Peer: inputPeerChannel(channel), ID: []int{1}, diff --git a/internal/rpc/messages_reports.go b/internal/rpc/messages_reports.go index 50688ce0..392fa207 100644 --- a/internal/rpc/messages_reports.go +++ b/internal/rpc/messages_reports.go @@ -9,6 +9,7 @@ import ( "github.com/iamxvbaba/td/tg" "github.com/iamxvbaba/td/tgerr" + compatandroid "telesrv/internal/compat/android" "telesrv/internal/domain" ) @@ -52,6 +53,14 @@ func (r *Router) onMessagesReport(ctx context.Context, req *tg.MessagesReportReq return nil, err } if len(req.ID) == 0 { + if compatandroid.OfferInitialMessageReportOptions( + string(ClientTypeFrom(ctx)), + len(req.ID), + req.Option, + req.Message, + ) { + return reportResultForOption("") + } return nil, tgerr.New(400, "MESSAGE_ID_REQUIRED") } if len(req.ID) > maxGetMessagesIDs || len(req.Option) > maxReportOptionLength || utf8.RuneCountInString(req.Message) > maxReportCommentLength {