-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtgbot.go
More file actions
366 lines (319 loc) · 10.3 KB
/
Copy pathtgbot.go
File metadata and controls
366 lines (319 loc) · 10.3 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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
package main
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"strconv"
)
// Telegram bot configuration
var (
tgBotToken string
tgWebhookSecret string
tgAppURL string
tgAPIBase string
tgBotUsername string
tgHTTPClient = &http.Client{}
)
// initTelegramBot reads env vars and registers the webhook.
// Returns true if the bot is enabled (TG_BOT_TOKEN is set).
func initTelegramBot() bool {
tgBotToken = os.Getenv("TG_BOT_TOKEN")
if tgBotToken == "" {
return false
}
tgAPIBase = "https://api.telegram.org/bot" + tgBotToken
tgWebhookSecret = os.Getenv("TG_WEBHOOK_SECRET")
if tgWebhookSecret == "" {
b := make([]byte, 16)
rand.Read(b)
tgWebhookSecret = hex.EncodeToString(b)
log.Printf("TG_WEBHOOK_SECRET auto-generated: %s", tgWebhookSecret)
}
tgAppURL = os.Getenv("TG_APP_URL")
if tgAppURL == "" {
tgAppURL = "https://zero.uswap.net"
}
// Register webhook
appURL := tgAppURL + "/tg/webhook/" + tgWebhookSecret
if err := tgSetWebhook(appURL); err != nil {
log.Printf("WARNING: Failed to set Telegram webhook: %v", err)
}
// Fetch bot info (needed for deep links)
tgGetMe()
// Set bot commands
tgSetCommands()
return true
}
// --- Telegram API Types ---
// TGUpdate represents an incoming update from Telegram.
type TGUpdate struct {
UpdateID int `json:"update_id"`
Message *TGMessage `json:"message,omitempty"`
CallbackQuery *TGCallbackQuery `json:"callback_query,omitempty"`
InlineQuery *TGInlineQuery `json:"inline_query,omitempty"`
ChosenInlineResult *TGChosenInlineResult `json:"chosen_inline_result,omitempty"`
}
// TGInlineQuery is received when a user types @botname in any chat.
type TGInlineQuery struct {
ID string `json:"id"`
From TGUser `json:"from"`
Query string `json:"query"`
Offset string `json:"offset"`
ChatType string `json:"chat_type,omitempty"`
}
// TGChosenInlineResult is sent when a user selects an inline result.
type TGChosenInlineResult struct {
ResultID string `json:"result_id"`
From TGUser `json:"from"`
Query string `json:"query"`
InlineMessageID string `json:"inline_message_id,omitempty"`
}
// TGInlineQueryResultArticle is a text-based inline query result.
type TGInlineQueryResultArticle struct {
Type string `json:"type"`
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
InputMessageContent TGInputTextMessageContent `json:"input_message_content"`
ReplyMarkup *TGInlineKeyboardMarkup `json:"reply_markup,omitempty"`
}
// TGInputTextMessageContent is the message content for an inline result.
type TGInputTextMessageContent struct {
MessageText string `json:"message_text"`
ParseMode string `json:"parse_mode,omitempty"`
LinkPreviewOptions map[string]interface{} `json:"link_preview_options,omitempty"`
}
// TGMessage represents a Telegram message.
type TGMessage struct {
MessageID int `json:"message_id"`
Chat TGChat `json:"chat"`
From *TGUser `json:"from,omitempty"`
Text string `json:"text,omitempty"`
ReplyTo *TGMessage `json:"reply_to_message,omitempty"`
}
// TGChat represents a Telegram chat.
type TGChat struct {
ID int64 `json:"id"`
Type string `json:"type"`
}
// TGUser represents a Telegram user.
type TGUser struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
Username string `json:"username,omitempty"`
}
// TGCallbackQuery represents a callback from an inline button press.
type TGCallbackQuery struct {
ID string `json:"id"`
From TGUser `json:"from"`
Message *TGMessage `json:"message,omitempty"`
Data string `json:"data"`
}
// TGInlineKeyboardMarkup holds inline keyboard buttons.
type TGInlineKeyboardMarkup struct {
InlineKeyboard [][]TGInlineKeyboardButton `json:"inline_keyboard"`
}
// TGInlineKeyboardButton is a single inline button.
// Style can be "primary" (blue), "danger" (red), or "success" (green) — Bot API 9.4+.
type TGInlineKeyboardButton struct {
Text string `json:"text"`
CallbackData string `json:"callback_data,omitempty"`
URL string `json:"url,omitempty"`
WebApp *TGWebApp `json:"web_app,omitempty"`
Style string `json:"style,omitempty"`
}
// TGWebApp opens a Mini App URL.
type TGWebApp struct {
URL string `json:"url"`
}
// TGForceReply forces the user to reply to a message.
type TGForceReply struct {
ForceReply bool `json:"force_reply"`
Selective bool `json:"selective"`
InputFieldPlaceholder string `json:"input_field_placeholder,omitempty"`
}
// TGAPIResponse is the generic Telegram API response wrapper.
type TGAPIResponse struct {
OK bool `json:"ok"`
Description string `json:"description,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
}
// TGSentMessage is the result of sendMessage/editMessage.
type TGSentMessage struct {
MessageID int `json:"message_id"`
Chat TGChat `json:"chat"`
}
// --- Telegram API Methods ---
// tgRequest makes a JSON POST to the Telegram Bot API.
func tgRequest(method string, payload interface{}) (json.RawMessage, error) {
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("tg marshal: %w", err)
}
resp, err := tgHTTPClient.Post(tgAPIBase+"/"+method, "application/json", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("tg request: %w", err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("tg read: %w", err)
}
var apiResp TGAPIResponse
if err := json.Unmarshal(data, &apiResp); err != nil {
return nil, fmt.Errorf("tg parse: %w", err)
}
if !apiResp.OK {
return nil, fmt.Errorf("tg API error: %s", apiResp.Description)
}
return apiResp.Result, nil
}
// tgSendMessage sends a text message with optional reply markup.
// Link previews are always disabled — the bot sends informational cards,
// not content where previews add value.
func tgSendMessage(chatID int64, text string, markup interface{}) (*TGSentMessage, error) {
payload := map[string]interface{}{
"chat_id": chatID,
"text": text,
"parse_mode": "HTML",
"link_preview_options": map[string]interface{}{
"is_disabled": true,
},
}
if markup != nil {
payload["reply_markup"] = markup
}
result, err := tgRequest("sendMessage", payload)
if err != nil {
return nil, err
}
var msg TGSentMessage
json.Unmarshal(result, &msg)
return &msg, nil
}
// tgEditMessage edits an existing message's text and markup.
// Link previews are always disabled.
func tgEditMessage(chatID int64, messageID int, text string, markup *TGInlineKeyboardMarkup) error {
payload := map[string]interface{}{
"chat_id": chatID,
"message_id": messageID,
"text": text,
"parse_mode": "HTML",
"link_preview_options": map[string]interface{}{
"is_disabled": true,
},
}
if markup != nil {
payload["reply_markup"] = markup
}
_, err := tgRequest("editMessageText", payload)
return err
}
// tgDeleteMessage deletes a message.
func tgDeleteMessage(chatID int64, messageID int) {
payload := map[string]interface{}{
"chat_id": chatID,
"message_id": messageID,
}
tgRequest("deleteMessage", payload)
}
// tgAnswerCallback answers a callback query with an optional toast text.
func tgAnswerCallback(callbackID string, text string) {
payload := map[string]interface{}{
"callback_query_id": callbackID,
}
if text != "" {
payload["text"] = text
}
tgRequest("answerCallbackQuery", payload)
}
// tgSendPhoto sends a photo (PNG bytes) with a caption and inline keyboard.
func tgSendPhoto(chatID int64, pngData []byte, caption string, markup *TGInlineKeyboardMarkup) (*TGSentMessage, error) {
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
w.WriteField("chat_id", strconv.FormatInt(chatID, 10))
w.WriteField("caption", caption)
w.WriteField("parse_mode", "HTML")
if markup != nil {
markupJSON, _ := json.Marshal(markup)
w.WriteField("reply_markup", string(markupJSON))
}
part, err := w.CreateFormFile("photo", "qr.png")
if err != nil {
return nil, fmt.Errorf("tg create form file: %w", err)
}
part.Write(pngData)
w.Close()
resp, err := tgHTTPClient.Post(tgAPIBase+"/sendPhoto", w.FormDataContentType(), &buf)
if err != nil {
return nil, fmt.Errorf("tg send photo: %w", err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("tg read photo resp: %w", err)
}
var apiResp TGAPIResponse
if err := json.Unmarshal(data, &apiResp); err != nil {
return nil, fmt.Errorf("tg parse photo resp: %w", err)
}
if !apiResp.OK {
return nil, fmt.Errorf("tg sendPhoto error: %s", apiResp.Description)
}
var msg TGSentMessage
json.Unmarshal(apiResp.Result, &msg)
return &msg, nil
}
// tgAnswerInlineQuery responds to an inline query with a list of results.
func tgAnswerInlineQuery(queryID string, results []interface{}, cacheTime int) {
payload := map[string]interface{}{
"inline_query_id": queryID,
"results": results,
"cache_time": cacheTime,
}
tgRequest("answerInlineQuery", payload)
}
// tgGetMe fetches the bot's own user info and stores the username.
func tgGetMe() {
result, err := tgRequest("getMe", map[string]interface{}{})
if err != nil {
return
}
var u TGUser
json.Unmarshal(result, &u)
if u.Username != "" {
tgBotUsername = u.Username
}
}
// tgSetWebhook registers the webhook URL with Telegram.
func tgSetWebhook(url string) error {
payload := map[string]interface{}{
"url": url,
"allowed_updates": []string{"message", "callback_query", "inline_query"},
}
_, err := tgRequest("setWebhook", payload)
if err != nil {
return err
}
log.Printf("Telegram webhook set to: %s", url)
return nil
}
// tgSetCommands registers the bot's command list.
func tgSetCommands() {
commands := []map[string]string{
{"command": "start", "description": "Start a new swap"},
{"command": "verify", "description": "Verify deployment integrity"},
{"command": "status", "description": "Check order status"},
}
payload := map[string]interface{}{
"commands": commands,
}
tgRequest("setMyCommands", payload)
}