Skip to content

Commit 2a18669

Browse files
author
cuiko tang
authored
Merge pull request #29 from cuiko/fix/msg-delete-affected-count
fix(msg): Report the count Telegram actually deleted, not requested
2 parents 0080250 + 00629b9 commit 2a18669

7 files changed

Lines changed: 83 additions & 29 deletions

File tree

internal/action/message/ops.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -433,8 +433,8 @@ type DeleteResult struct {
433433
Count int
434434
}
435435

436-
// DeleteFunc deletes messages.
437-
type DeleteFunc func(context.Context, DeleteQuery) error
436+
// DeleteFunc deletes messages and returns how many Telegram actually affected.
437+
type DeleteFunc func(context.Context, DeleteQuery) (int, error)
438438

439439
// Delete validates, confirms, and dispatches a delete request.
440440
func Delete(ctx context.Context, req DeleteRequest, do DeleteFunc) (DeleteResult, error) {
@@ -452,14 +452,18 @@ func Delete(ctx context.Context, req DeleteRequest, do DeleteFunc) (DeleteResult
452452
if err := ui.ConfirmDestructive(req.Prompter, fmt.Sprintf("%s %d message(s) in %s?", verb, len(query.IDs), query.Ref.String()), req.Yes); err != nil {
453453
return DeleteResult{}, err
454454
}
455-
if err := do(ctx, query); err != nil {
455+
affected, err := do(ctx, query)
456+
if err != nil {
456457
return DeleteResult{}, err
457458
}
458459
outVerb := "deleted"
459460
if query.Revoke {
460461
outVerb = "revoked"
461462
}
462-
return DeleteResult{Verb: outVerb, Count: len(query.IDs)}, nil
463+
// Report the count Telegram actually affected, not the number requested:
464+
// a self-side delete of another user's message in a supergroup is a no-op
465+
// the server reports as 0.
466+
return DeleteResult{Verb: outVerb, Count: affected}, nil
463467
}
464468

465469
// NormalizeDelete parses delete refs.

internal/action/message/ops_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -250,24 +250,26 @@ func TestDelete_ConfirmsBeforeDispatch(t *testing.T) {
250250
result, err := actionmessage.Delete(context.Background(), actionmessage.DeleteRequest{
251251
RawMessageRefs: []string{"@chat:1", "@chat:2"},
252252
Prompter: stubPrompter{ok: true},
253-
}, func(_ context.Context, q actionmessage.DeleteQuery) error {
253+
}, func(_ context.Context, q actionmessage.DeleteQuery) (int, error) {
254254
called = true
255255
require.Equal(t, []int{1, 2}, q.IDs)
256-
return nil
256+
// Server reports only one affected though two IDs were requested; the
257+
// result count must come from this return value, not len(q.IDs).
258+
return 1, nil
257259
})
258260
require.NoError(t, err)
259261
require.True(t, called)
260-
require.Equal(t, actionmessage.DeleteResult{Verb: "deleted", Count: 2}, result)
262+
require.Equal(t, actionmessage.DeleteResult{Verb: "deleted", Count: 1}, result)
261263
}
262264

263265
func TestDelete_DeclineSkipsDispatch(t *testing.T) {
264266
called := false
265267
_, err := actionmessage.Delete(context.Background(), actionmessage.DeleteRequest{
266268
RawMessageRefs: []string{"@chat:1"},
267269
Prompter: stubPrompter{ok: false},
268-
}, func(context.Context, actionmessage.DeleteQuery) error {
270+
}, func(context.Context, actionmessage.DeleteQuery) (int, error) {
269271
called = true
270-
return nil
272+
return 0, nil
271273
})
272274
require.ErrorIs(t, err, command.ErrNotConfirmed)
273275
require.False(t, called)

internal/cli/msg/delete/delete.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package deletecmd
33

44
import (
55
"context"
6+
"encoding/json"
67
"fmt"
78

89
"github.com/gotd/td/telegram/peers"
@@ -72,19 +73,37 @@ func Run(ctx context.Context, opts *Options) error {
7273

7374
// newDelete returns the production Delete closure that calls the Telegram API.
7475
func newDelete(f *runtime.Invocation) actionmessage.DeleteFunc {
75-
return func(ctx context.Context, q actionmessage.DeleteQuery) error {
76+
return func(ctx context.Context, q actionmessage.DeleteQuery) (int, error) {
7677
acct, err := f.Account("")
7778
if err != nil {
78-
return err
79+
return 0, err
7980
}
8081
if cl, _ := runtime.MaybeDialDaemon(ctx, f, acct); cl != nil {
8182
defer func() { _ = cl.Close() }()
82-
_, err := cl.Call(ctx, "msg.delete", q)
83-
return err
83+
raw, err := cl.Call(ctx, "msg.delete", q)
84+
if err != nil {
85+
return 0, err
86+
}
87+
return decodeDeleteCount(raw, len(q.IDs)), nil
8488
}
85-
return f.WithPeers(ctx, acct, runtime.ClientOptsFrom(f, acct),
89+
var affected int
90+
err = f.WithPeers(ctx, acct, runtime.ClientOptsFrom(f, acct),
8691
func(ctx context.Context, api *tg.Client, _ *peers.Manager, res *peer.Resolver) error {
87-
return telegram.DeleteMessages(ctx, api, res, q)
92+
affected, err = telegram.DeleteMessages(ctx, api, res, q)
93+
return err
8894
})
95+
return affected, err
96+
}
97+
}
98+
99+
// decodeDeleteCount reads the affected-message count from a daemon msg.delete
100+
// response. A current daemon returns the integer count; an older one returns a
101+
// bare "true" ack that won't decode, in which case we fall back to the
102+
// requested count so delete still works across a version skew.
103+
func decodeDeleteCount(raw json.RawMessage, requested int) int {
104+
var affected int
105+
if err := json.Unmarshal(raw, &affected); err != nil {
106+
return requested
89107
}
108+
return affected
90109
}

internal/cli/msg/delete/delete_test.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func TestRun_NoYesNoPrompter_Declined(t *testing.T) {
3030
f.Prompter = stubPrompter{ok: false}
3131
opts := &del.Options{
3232
RawMessageRefs: []string{"@a:1"}, Prompter: f.Prompter, IOStreams: ios,
33-
Delete: func(_ context.Context, _ actionmessage.DeleteQuery) error { return nil },
33+
Delete: func(_ context.Context, _ actionmessage.DeleteQuery) (int, error) { return 0, nil },
3434
}
3535
err := del.Run(context.Background(), opts)
3636
require.ErrorIs(t, err, command.ErrNotConfirmed)
@@ -42,11 +42,13 @@ func TestRun_YesSkipsPromptCallsDelete(t *testing.T) {
4242
called := false
4343
opts := &del.Options{
4444
RawMessageRefs: []string{"@a:1", "@a:2"}, Yes: true, Prompter: f.Prompter, IOStreams: ios,
45-
Delete: func(_ context.Context, _ actionmessage.DeleteQuery) error { called = true; return nil },
45+
// Two refs requested but Telegram reports only one affected — the
46+
// output must reflect the affected count, not len(refs).
47+
Delete: func(_ context.Context, _ actionmessage.DeleteQuery) (int, error) { called = true; return 1, nil },
4648
}
4749
require.NoError(t, del.Run(context.Background(), opts))
4850
require.True(t, called)
49-
require.Contains(t, stdout.String(), "deleted")
51+
require.Contains(t, stdout.String(), "deleted\t1")
5052
}
5153

5254
func TestRun_PromptAcceptedCallsDelete(t *testing.T) {
@@ -56,7 +58,7 @@ func TestRun_PromptAcceptedCallsDelete(t *testing.T) {
5658
called := false
5759
opts := &del.Options{
5860
RawMessageRefs: []string{"@a:5"}, Prompter: f.Prompter, IOStreams: ios,
59-
Delete: func(_ context.Context, _ actionmessage.DeleteQuery) error { called = true; return nil },
61+
Delete: func(_ context.Context, _ actionmessage.DeleteQuery) (int, error) { called = true; return 1, nil },
6062
}
6163
require.NoError(t, del.Run(context.Background(), opts))
6264
require.True(t, called)
@@ -76,13 +78,21 @@ func TestRun_RevokeOutputsRevoked(t *testing.T) {
7678
f := runtime.NewTestInvocation(t)
7779
opts := &del.Options{
7880
RawMessageRefs: []string{"@a:1"}, Revoke: true, Yes: true, Prompter: f.Prompter, IOStreams: ios,
79-
Delete: func(_ context.Context, _ actionmessage.DeleteQuery) error { return nil },
81+
Delete: func(_ context.Context, _ actionmessage.DeleteQuery) (int, error) { return 1, nil },
8082
}
8183
require.NoError(t, del.Run(context.Background(), opts))
8284
require.Contains(t, stdout.String(), "revoked")
8385
require.NotContains(t, stdout.String(), "deleted")
8486
}
8587

88+
func TestDecodeDeleteCount(t *testing.T) {
89+
// Current daemon: integer affected count is used as-is.
90+
require.Equal(t, 3, del.DecodeDeleteCountForTest([]byte("3"), 5))
91+
require.Equal(t, 0, del.DecodeDeleteCountForTest([]byte("0"), 5))
92+
// Older daemon: bare "true" ack doesn't decode -> fall back to requested.
93+
require.Equal(t, 5, del.DecodeDeleteCountForTest([]byte("true"), 5))
94+
}
95+
8696
type stubPrompter struct{ ok bool }
8797

8898
func (s stubPrompter) Confirm(string, bool) (bool, error) { return s.ok, nil }
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package deletecmd
2+
3+
import "encoding/json"
4+
5+
// DecodeDeleteCountForTest exposes decodeDeleteCount to the external test
6+
// package.
7+
func DecodeDeleteCountForTest(raw json.RawMessage, requested int) int {
8+
return decodeDeleteCount(raw, requested)
9+
}

internal/daemon/handlers.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,11 @@ func registerHandlers(
155155
if err := json.Unmarshal(params, &q); err != nil { //nolint:musttag
156156
return nil, fmt.Errorf("invalid msg.delete params: %w", err)
157157
}
158-
if err := telegram.DeleteMessages(ctx, api, res, q); err != nil {
158+
affected, err := telegram.DeleteMessages(ctx, api, res, q)
159+
if err != nil {
159160
return nil, err
160161
}
161-
return json.RawMessage("true"), nil
162+
return json.Marshal(affected)
162163
})
163164

164165
srv.Register("msg.pin", func(ctx context.Context, params json.RawMessage) (json.RawMessage, error) {

internal/telegram/message_ops.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -292,20 +292,29 @@ func ForwardMessages(ctx context.Context, api *tg.Client, resolver *peer.Resolve
292292
return rows[0], nil
293293
}
294294

295-
// DeleteMessages performs the Telegram RPC for `tg msg delete`.
296-
func DeleteMessages(ctx context.Context, api *tg.Client, resolver *peer.Resolver, q actionmessage.DeleteQuery) error {
295+
// DeleteMessages performs the Telegram RPC for `tg msg delete`. It returns the
296+
// number of messages Telegram actually affected (PtsCount), which can be less
297+
// than len(q.IDs) — e.g. deleting someone else's message self-side in a
298+
// supergroup is a no-op the server reports as 0.
299+
func DeleteMessages(ctx context.Context, api *tg.Client, resolver *peer.Resolver, q actionmessage.DeleteQuery) (int, error) {
297300
sender := gotdmessage.NewSender(api)
298301
if !q.Revoke {
299-
_, err := sender.Delete().Messages(ctx, q.IDs...)
300-
return err
302+
affected, err := sender.Delete().Messages(ctx, q.IDs...)
303+
if err != nil {
304+
return 0, err
305+
}
306+
return affected.PtsCount, nil
301307
}
302308

303309
resolved, err := resolver.Resolve(ctx, q.Ref)
304310
if err != nil {
305-
return err
311+
return 0, err
306312
}
307-
_, err = sender.To(resolved.InputPeer).Revoke().Messages(ctx, q.IDs...)
308-
return err
313+
affected, err := sender.To(resolved.InputPeer).Revoke().Messages(ctx, q.IDs...)
314+
if err != nil {
315+
return 0, err
316+
}
317+
return affected.PtsCount, nil
309318
}
310319

311320
// PinMessage performs the Telegram RPC for `tg msg pin` and `tg msg unpin`.

0 commit comments

Comments
 (0)