diff --git a/README.md b/README.md index bcfc3d8..20e5665 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ client commands route through when present). | `tg chat list` | List dialogs. | | `tg chat info ` | Show one user, chat, bot, or channel. `--full` adds members/admins/online counts, about, linked discussion group, pinned message, and slow mode (supergroups/channels only). | | `tg chat create ` | Create a supergroup. `--forum` enables topics; `--about` sets the description. | -| `tg chat delete <ref>` | Delete a supergroup or channel (irreversible). Prompts unless `--yes`. | +| `tg chat delete <ref>` | Delete a supergroup/channel (irreversible), or remove a user DM from your chat list (`--revoke` also deletes it on the other side). Prompts unless `--yes`. | | `tg chat edit <ref>` | Edit a supergroup: `--title`, `--about`, `--public <name>` / `--private`; toggles: `--forum`/`--no-forum`, `--hide-members`/`--show-members`, `--hide-history`/`--show-history`, `--slow-mode <s>`, `--no-forwards`/`--allow-forwards`, `--need-approval`/`--no-need-approval` (public only). | | `tg chat topic list <ref>` | List a forum supergroup's topics. `--search` filters by title; `--limit` caps results (single page only, ~100 max — pagination not implemented). | | `tg chat topic create <ref> <title>` | Create a topic. `--icon-color`, `--icon-emoji`, and `--random-id` (idempotent retry). | diff --git a/internal/action/chat/delete.go b/internal/action/chat/delete.go index 39c462d..b5d4417 100644 --- a/internal/action/chat/delete.go +++ b/internal/action/chat/delete.go @@ -13,21 +13,24 @@ import ( // DeleteChatRequest is the raw request for `tg chat delete`. type DeleteChatRequest struct { RawRef string + Revoke bool Yes bool Prompter ui.Prompter } // DeleteChatQuery is the normalized payload passed to Telegram. type DeleteChatQuery struct { - Ref ref.Ref + Ref ref.Ref + Revoke bool } -// DeleteChatFunc deletes a supergroup or channel. +// DeleteChatFunc deletes a channel/supergroup or removes a user DM. type DeleteChatFunc func(context.Context, DeleteChatQuery) (output.PeerRef, error) // DeleteChat validates, confirms, and dispatches a chat-delete request. -// Deleting a channel/supergroup is irreversible and affects every member, so -// it always confirms unless Yes is set. +// Deleting a channel/supergroup is irreversible and affects every member; +// deleting a user DM removes the conversation from the account (and, with +// Revoke, the other side too). Either way it confirms unless Yes is set. func DeleteChat(ctx context.Context, req DeleteChatRequest, do DeleteChatFunc) (output.PeerRef, error) { parsed, err := ref.ParseRef(req.RawRef) if err != nil { @@ -36,8 +39,12 @@ func DeleteChat(ctx context.Context, req DeleteChatRequest, do DeleteChatFunc) ( if do == nil { return output.PeerRef{}, fmt.Errorf("%w: chat delete called without delete function", command.ErrPrecondition) } - if err := ui.ConfirmDestructive(req.Prompter, fmt.Sprintf("delete %s for everyone? this cannot be undone", parsed.String()), req.Yes); err != nil { + prompt := fmt.Sprintf("delete %s? this cannot be undone", parsed.String()) + if req.Revoke { + prompt = fmt.Sprintf("delete %s for everyone? this cannot be undone", parsed.String()) + } + if err := ui.ConfirmDestructive(req.Prompter, prompt, req.Yes); err != nil { return output.PeerRef{}, err } - return do(ctx, DeleteChatQuery{Ref: parsed}) + return do(ctx, DeleteChatQuery{Ref: parsed, Revoke: req.Revoke}) } diff --git a/internal/action/chat/delete_test.go b/internal/action/chat/delete_test.go index 5dd1f30..e4cd45d 100644 --- a/internal/action/chat/delete_test.go +++ b/internal/action/chat/delete_test.go @@ -36,3 +36,16 @@ func TestDeleteChat_DeclineSkipsDispatch(t *testing.T) { }) require.ErrorIs(t, err, command.ErrNotConfirmed) } + +func TestDeleteChat_RevokePassesThrough(t *testing.T) { + _, err := actionchat.DeleteChat(context.Background(), actionchat.DeleteChatRequest{ + RawRef: "@bob", + Revoke: true, + Prompter: &ui.StubPrompter{Answers: []any{true}}, + }, func(_ context.Context, q actionchat.DeleteChatQuery) (output.PeerRef, error) { + require.Equal(t, "bob", q.Ref.Value) + require.True(t, q.Revoke) + return output.PeerRef{}, nil + }) + require.NoError(t, err) +} diff --git a/internal/cli/chat/delete/delete.go b/internal/cli/chat/delete/delete.go index 3dcbf28..491741a 100644 --- a/internal/cli/chat/delete/delete.go +++ b/internal/cli/chat/delete/delete.go @@ -22,6 +22,7 @@ import ( // Options holds the resolved flags and injected dependencies for Run. type Options struct { RawRef string + Revoke bool Yes bool Exporter output.Exporter IOStreams *ui.IOStreams @@ -31,7 +32,7 @@ type Options struct { // New builds the cobra command for "tg chat delete". func New(f *runtime.Invocation, runF func(*Options) error) *cobra.Command { - return NewWith(f, runF, "Delete a supergroup or channel (irreversible)") + return NewWith(f, runF, "Delete a supergroup/channel, or remove a user DM from your chat list") } // NewWith builds the delete command with a caller-supplied short description so @@ -55,6 +56,7 @@ func NewWith(f *runtime.Invocation, runF func(*Options) error, short string) *co return Run(cmd.Context(), opts) }, } + cmd.Flags().BoolVar(&opts.Revoke, "revoke", false, "For a user DM, also delete the conversation on the other side") cmd.Flags().BoolVarP(&opts.Yes, "yes", "y", false, "Skip confirmation prompt") command.SetMeta(cmd, command.Meta{NeedsAccount: true, NeedsClient: true}) output.AddJSONFlags(cmd, &opts.Exporter, []string{"id", "ref", "kind", "title", "username"}) @@ -65,6 +67,7 @@ func NewWith(f *runtime.Invocation, runF func(*Options) error, short string) *co func Run(ctx context.Context, opts *Options) error { pr, err := actionchat.DeleteChat(ctx, actionchat.DeleteChatRequest{ RawRef: opts.RawRef, + Revoke: opts.Revoke, Yes: opts.Yes, Prompter: opts.Prompter, }, opts.Delete) diff --git a/internal/telegram/chat_delete.go b/internal/telegram/chat_delete.go index 8b43787..ffb1182 100644 --- a/internal/telegram/chat_delete.go +++ b/internal/telegram/chat_delete.go @@ -12,19 +12,30 @@ import ( "github.com/vika2603/telegram-cli/internal/telegram/peer" ) -// DeleteChat performs the RPC for `tg chat delete`: it deletes a supergroup or -// channel via channels.deleteChannel (irreversible, creator-only). +// DeleteChat performs the RPC for `tg chat delete`. A supergroup/channel is +// deleted via channels.deleteChannel (irreversible, creator-only). A user/bot +// DM is removed from the account via messages.deleteHistory (with q.Revoke it +// also deletes the history on the other side). Basic groups are not handled — +// use `tg chat leave`. func DeleteChat(ctx context.Context, api *tg.Client, resolver *peer.Resolver, q actionchat.DeleteChatQuery) (output.PeerRef, error) { resolved, err := resolver.Resolve(ctx, q.Ref) if err != nil { return output.PeerRef{}, err } - inCh, ok := inputChannelFromPeer(resolved.InputPeer) - if !ok { - return output.PeerRef{}, fmt.Errorf("%w: only supergroups and channels can be deleted by ref", command.ErrUnsupported) + if inCh, ok := inputChannelFromPeer(resolved.InputPeer); ok { + if _, err := api.ChannelsDeleteChannel(ctx, inCh); err != nil { + return output.PeerRef{}, err + } + return output.PeerRefFromResolved(resolved), nil } - if _, err := api.ChannelsDeleteChannel(ctx, inCh); err != nil { - return output.PeerRef{}, err + if resolved.Kind == "user" || resolved.Kind == "bot" { + if _, err := api.MessagesDeleteHistory(ctx, &tg.MessagesDeleteHistoryRequest{ + Peer: resolved.InputPeer, + Revoke: q.Revoke, + }); err != nil { + return output.PeerRef{}, err + } + return output.PeerRefFromResolved(resolved), nil } - return output.PeerRefFromResolved(resolved), nil + return output.PeerRef{}, fmt.Errorf("%w: only channels, supergroups, and user DMs can be deleted by ref (use `tg chat leave` for basic groups)", command.ErrUnsupported) }