all repos

rss-tools @ 8c0c68e023cfb8e513f5d9d072ab47ac61b5bb65

get rss feed from sources that(i need and) dont provide one

rss-tools/sources/telegram/sdk.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
telegram: preserve \n in multiline messages, support multi image messages, 14 days ago
1
package telegram
2
3
import (
4
	"bytes"
5
	"context"
6
	"encoding/base64"
7
	"encoding/json"
8
	"fmt"
9
	"io"
10
	"net/http"
11
	"net/url"
12
	"strconv"
13
	"strings"
14
)
15
16
const (
17
	apiBase       = "https://api.telegram.org"
18
	maxPhotoBytes = 20 << 20
19
)
20
21
type TelegramSDK struct {
22
	client *http.Client
23
	token  string
24
}
25
26
func NewSDK(client *http.Client, token string) *TelegramSDK {
27
	return &TelegramSDK{
28
		token:  token,
29
		client: client,
30
	}
31
}
32
33
type Response[T any] struct {
34
	OK          bool   `json:"ok"`
35
	Result      T      `json:"result"`
36
	Description string `json:"description"`
37
}
38
39
type Update struct {
40
	UpdateID int64    `json:"update_id"`
41
	Message  *Message `json:"message"`
42
}
43
44
type User struct {
45
	ID        int64  `json:"id"`
46
	FirstName string `json:"first_name"`
47
	Username  string `json:"username"`
48
}
49
50
type Chat struct {
51
	ID int64 `json:"id"`
52
}
53
54
type Message struct {
55
	MessageID        int64             `json:"message_id"`
56
	From             *User             `json:"from"`
57
	Chat             *Chat             `json:"chat"`
58
	Text             string            `json:"text"`
59
	Caption          string            `json:"caption,omitempty"`
60
	Date             int64             `json:"date"`
61
	MediaGroupID     string            `json:"media_group_id,omitempty"`
62
	Photo            []PhotoSize       `json:"photo,omitempty"`
63
	PhotoBase64      string            `json:"photo_base64,omitempty"`
64
	PhotoMIMEType    string            `json:"photo_mime_type,omitempty"`
65
	PhotoAttachments []PhotoAttachment `json:"photo_attachments,omitempty"`
66
	LinkTitles       map[string]string `json:"-"`
67
}
68
69
type PhotoAttachment struct {
70
	Base64   string `json:"base64,omitempty"`
71
	MIMEType string `json:"mime_type,omitempty"`
72
}
73
74
type PhotoSize struct {
75
	FileID   string `json:"file_id"`
76
	Width    int    `json:"width"`
77
	Height   int    `json:"height"`
78
	FileSize int64  `json:"file_size"`
79
}
80
81
func (t *TelegramSDK) GetUpdates(ctx context.Context, offset int64) ([]Update, error) {
82
	params := url.Values{}
83
	params.Set("offset", strconv.FormatInt(offset, 10))
84
	params.Set("timeout", "30")
85
86
	var resp Response[[]Update]
87
	if err := t.req(ctx, "getUpdates", params, nil, &resp); err != nil {
88
		return nil, err
89
	}
90
91
	for i := range resp.Result {
92
		msg := resp.Result[i].Message
93
		if msg == nil || len(msg.Photo) == 0 {
94
			continue
95
		}
96
		data, mimeType, err := t.downloadLargestPhoto(ctx, msg.Photo)
97
		if err != nil {
98
			return nil, err
99
		}
100
		attachment := PhotoAttachment{
101
			Base64:   base64.StdEncoding.EncodeToString(data),
102
			MIMEType: mimeType,
103
		}
104
		msg.PhotoAttachments = append(msg.PhotoAttachments, attachment)
105
		if msg.PhotoBase64 == "" {
106
			msg.PhotoBase64 = attachment.Base64
107
			msg.PhotoMIMEType = attachment.MIMEType
108
		}
109
	}
110
	return resp.Result, nil
111
}
112
113
type messageReactionReq struct {
114
	Type  string `json:"type"`
115
	Emoji string `json:"emoji"`
116
}
117
118
type setReactionReq struct {
119
	ChatID    int64                `json:"chat_id"`
120
	MessageID int64                `json:"message_id"`
121
	Reaction  []messageReactionReq `json:"reaction"`
122
}
123
124
func (t *TelegramSDK) SetReaction(ctx context.Context, chatID, messageID int64, emoji string) error {
125
	var resp Response[bool]
126
	return t.req(ctx, "setMessageReaction", nil, setReactionReq{
127
		ChatID:    chatID,
128
		MessageID: messageID,
129
		Reaction:  []messageReactionReq{{Type: "emoji", Emoji: emoji}},
130
	}, &resp)
131
}
132
133
type tgFile struct {
134
	FilePath string `json:"file_path"`
135
}
136
137
func (t *TelegramSDK) downloadLargestPhoto(ctx context.Context, photos []PhotoSize) ([]byte, string, error) {
138
	if len(photos) == 0 {
139
		return nil, "", nil
140
	}
141
142
	filePath, err := t.getFilePath(ctx, photos[len(photos)-1].FileID)
143
	if err != nil {
144
		return nil, "", err
145
	}
146
147
	fileURL := fmt.Sprintf("%s/file/bot%s/%s", apiBase, t.token, filePath)
148
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileURL, nil)
149
	if err != nil {
150
		return nil, "", err
151
	}
152
153
	res, err := t.client.Do(req)
154
	if err != nil {
155
		return nil, "", err
156
	}
157
	defer res.Body.Close()
158
	if res.StatusCode != http.StatusOK {
159
		return nil, "", fmt.Errorf("photo download failed with status %d", res.StatusCode)
160
	}
161
162
	data, err := io.ReadAll(io.LimitReader(res.Body, maxPhotoBytes+1))
163
	if err != nil {
164
		return nil, "", err
165
	}
166
	if len(data) > maxPhotoBytes {
167
		return nil, "", fmt.Errorf("photo too large: %d bytes", len(data))
168
	}
169
170
	mimeType := http.DetectContentType(data)
171
	if !strings.HasPrefix(mimeType, "image/") {
172
		mimeType = "image/jpeg"
173
	}
174
	return data, mimeType, nil
175
}
176
177
func (t *TelegramSDK) getFilePath(ctx context.Context, fileID string) (string, error) {
178
	params := url.Values{}
179
	params.Set("file_id", fileID)
180
181
	var resp Response[tgFile]
182
	if err := t.req(ctx, "getFile", params, nil, &resp); err != nil {
183
		return "", err
184
	}
185
	return resp.Result.FilePath, nil
186
}
187
188
func (t *TelegramSDK) req(ctx context.Context, method string, params url.Values, body any, out any) error {
189
	u := fmt.Sprintf("%s/bot%s/%s", apiBase, t.token, method)
190
	if params != nil {
191
		u += "?" + params.Encode()
192
	}
193
194
	var req *http.Request
195
	var err error
196
	if body != nil {
197
		var data []byte
198
		data, err = json.Marshal(body)
199
		if err != nil {
200
			return err
201
		}
202
		req, err = http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewReader(data))
203
		if err != nil {
204
			return err
205
		}
206
		req.Header.Set("Content-Type", "application/json")
207
	} else {
208
		req, err = http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
209
		if err != nil {
210
			return err
211
		}
212
	}
213
214
	res, err := t.client.Do(req)
215
	if err != nil {
216
		return err
217
	}
218
	defer res.Body.Close()
219
	return json.NewDecoder(res.Body).Decode(out)
220
}