forked from vika2603/telegram-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsend.go
More file actions
203 lines (187 loc) · 6.67 KB
/
Copy pathsend.go
File metadata and controls
203 lines (187 loc) · 6.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
// Package send implements "tg msg send <ref> [text...]".
package send
import (
"context"
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
"github.com/gotd/td/telegram/peers"
"github.com/gotd/td/tg"
"github.com/spf13/cobra"
"github.com/vika2603/telegram-cli/internal/account"
actionmessage "github.com/vika2603/telegram-cli/internal/action/message"
"github.com/vika2603/telegram-cli/internal/cli/complete"
"github.com/vika2603/telegram-cli/internal/command"
"github.com/vika2603/telegram-cli/internal/output"
"github.com/vika2603/telegram-cli/internal/runtime"
"github.com/vika2603/telegram-cli/internal/telegram"
"github.com/vika2603/telegram-cli/internal/telegram/peer"
"github.com/vika2603/telegram-cli/internal/ui"
)
// Options holds the resolved flags and injected dependencies for Run.
type Options struct {
RawRef string
Text string
Files []string
Names []string
Sticker string
Gif string
ReplyTo int
Silent bool
Schedule time.Time
Parse string
RandomID int64
Exporter output.Exporter
IOStreams *ui.IOStreams
Stdin io.Reader
// Send is the closure that performs the actual Telegram call. Production
// code sets it via newSend; tests stub it directly.
Send actionmessage.SendFunc
}
// New builds the cobra command for "tg msg send".
func New(f *runtime.Invocation, runF func(*Options) error) *cobra.Command {
opts := &Options{}
var scheduleRaw string
cmd := &cobra.Command{
Use: "send <ref> [text...]",
Short: "Send text and optional media to a chat",
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: complete.PeerRefs(f),
PreRunE: func(cmd *cobra.Command, args []string) error {
if scheduleRaw != "" {
t, err := time.Parse(time.RFC3339, scheduleRaw)
if err != nil {
return fmt.Errorf("%w: --schedule must be RFC3339: %s", command.ErrUsage, err.Error())
}
opts.Schedule = t
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
opts.RawRef = args[0]
opts.Text = strings.Join(args[1:], " ")
opts.IOStreams = f.IOStreams
opts.Stdin = f.IOStreams.In
if runF != nil {
return runF(opts)
}
opts.Send = newSend(f)
return Run(cmd.Context(), opts)
},
}
cmd.Flags().StringArrayVar(&opts.Files, "file", nil, `File attachment; repeat for multiple files; "-" reads stdin bytes`)
cmd.Flags().StringArrayVar(&opts.Names, "name", nil, "Upload filename override; repeat to match --file")
cmd.Flags().StringVar(&opts.Sticker, "sticker", "", "Send a sticker by `msg sticker list` ref or message ref (no text/--file)")
cmd.Flags().StringVar(&opts.Gif, "gif", "", "Send a gif by `msg gif list` ref or message ref (no text/--file)")
cmd.Flags().IntVar(&opts.ReplyTo, "reply-to", 0, "Reply to message ID")
cmd.Flags().BoolVar(&opts.Silent, "silent", false, "Send without notification")
cmd.Flags().StringVar(&scheduleRaw, "schedule", "", "Schedule delivery (RFC3339)")
cmd.Flags().StringVar(&opts.Parse, "parse", "", "Parse mode for text or caption (only: html)")
cmd.Flags().Int64Var(&opts.RandomID, "random-id", 0, "Idempotency key (int64): reusing it on retry dedupes the send server-side")
command.SetMeta(cmd, command.Meta{NeedsAccount: true, NeedsClient: true})
output.AddJSONFlags(cmd, &opts.Exporter,
[]string{"action", "message_id", "chat_id", "date"})
return cmd
}
// Run dispatches the normalized request and renders the result.
func Run(ctx context.Context, opts *Options) error {
rows, err := actionmessage.Send(ctx, actionmessage.SendRequest{
RawRef: opts.RawRef,
Text: opts.Text,
Files: opts.Files,
Names: opts.Names,
Sticker: opts.Sticker,
Gif: opts.Gif,
ReplyTo: opts.ReplyTo,
Silent: opts.Silent,
Schedule: opts.Schedule,
Parse: opts.Parse,
RandomID: opts.RandomID,
Stdin: opts.Stdin,
}, opts.Send)
if err != nil {
return err
}
if opts.Exporter != nil {
return opts.Exporter.Write(opts.IOStreams, rows)
}
return output.RenderSendResults(opts.IOStreams, rows)
}
// newSend returns the production Send closure that calls the Telegram API.
//
// Daemon fast-path: text/metadata sends always route through a reachable
// daemon; sticker/GIF sends route through it only when the daemon advertised
// the media-send capability (older daemons would drop the field and post an
// empty message). File attachments carry bytes the IPC socket cannot relay, so
// they always take the local WithPeers path.
func newSend(f *runtime.Invocation) actionmessage.SendFunc {
return func(ctx context.Context, q actionmessage.SendQuery) ([]output.SendResultRow, error) {
acct, err := f.Account("")
if err != nil {
return nil, err
}
if len(q.Attachments) == 0 {
if cl, _ := runtime.MaybeDialDaemon(ctx, f, acct); cl != nil {
if isMediaSend(q) && !cl.SupportsMediaSend() {
_ = cl.Close()
return nil, fmt.Errorf("%w: the running daemon is too old to relay stickers/GIFs; "+
"run `tg daemon restart` to enable daemon media sends, or `tg daemon stop` to send locally", account.ErrBusy)
}
defer func() { _ = cl.Close() }()
// SendQuery.Stdin is io.Reader, which the JSON encoder
// renders as `{}` and the decoder cannot rehydrate. Strip
// it before sending — telegram.SendMessage only consumes
// Stdin via Attachment.Path == "-", excluded above.
wire := q
wire.Stdin = nil
raw, err := cl.Call(ctx, "msg.send", wire)
if err != nil {
return nil, err
}
var rows []output.SendResultRow
if err := json.Unmarshal(raw, &rows); err != nil {
return nil, err
}
if store, err := account.OpenRecentStore(acct.Meta.Name); err == nil {
recordSentMessages(store, q.Ref.String(), q.Text, rows)
}
return rows, nil
}
}
var rows []output.SendResultRow
err = f.WithPeers(ctx, acct, runtime.ClientOptsFrom(f, acct),
func(ctx context.Context, api *tg.Client, _ *peers.Manager, res *peer.Resolver) error {
rows, err = telegram.SendMessage(ctx, api, res, q)
if err == nil {
recordSentMessages(res.Store(), q.Ref.String(), q.Text, rows)
}
return err
})
return rows, err
}
}
// isMediaSend reports whether the query carries a sticker or GIF, which a
// daemon can only relay when it advertised FeatureMediaSend.
func isMediaSend(q actionmessage.SendQuery) bool {
return q.Sticker != nil || q.Gif != nil
}
func recordSentMessages(store *account.PeerStore, peerRef, text string, rows []output.SendResultRow) {
if store == nil {
return
}
for _, row := range rows {
if row.MessageID <= 0 {
continue
}
_ = store.RecordRecentMessage(account.RecentMessage{
Ref: peerRef + ":" + strconv.Itoa(row.MessageID),
PeerRef: peerRef,
MessageID: row.MessageID,
Date: row.Date,
Text: text,
})
}
}