all repos

rss-tools @ 4f31c23166ead37fa81152dd4de985cfde107dfb

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: support images, 1 month 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
	Date          int64       `json:"date"`
60
	Photo         []PhotoSize `json:"photo,omitempty"`
61
	PhotoBase64   string      `json:"photo_base64,omitempty"`
62
	PhotoMIMEType string      `json:"photo_mime_type,omitempty"`
63
}
64
65
type PhotoSize struct {
66
	FileID   string `json:"file_id"`
67
	Width    int    `json:"width"`
68
	Height   int    `json:"height"`
69
	FileSize int64  `json:"file_size"`
70
}
71
72
func (t *TelegramSDK) GetUpdates(ctx context.Context, offset int64) ([]Update, error) {
73
	params := url.Values{}
74
	params.Set("offset", strconv.FormatInt(offset, 10))
75
	params.Set("timeout", "30")
76
77
	var resp Response[[]Update]
78
	if err := t.req(ctx, "getUpdates", params, nil, &resp); err != nil {
79
		return nil, err
80
	}
81
82
	for i := range resp.Result {
83
		msg := resp.Result[i].Message
84
		if msg == nil || len(msg.Photo) == 0 {
85
			continue
86
		}
87
		data, mimeType, err := t.downloadLargestPhoto(ctx, msg.Photo)
88
		if err != nil {
89
			return nil, err
90
		}
91
		msg.PhotoBase64 = base64.StdEncoding.EncodeToString(data)
92
		msg.PhotoMIMEType = mimeType
93
	}
94
	return resp.Result, nil
95
}
96
97
type messageReactionReq struct {
98
	Type  string `json:"type"`
99
	Emoji string `json:"emoji"`
100
}
101
102
type setReactionReq struct {
103
	ChatID    int64                `json:"chat_id"`
104
	MessageID int64                `json:"message_id"`
105
	Reaction  []messageReactionReq `json:"reaction"`
106
}
107
108
func (t *TelegramSDK) SetReaction(ctx context.Context, chatID, messageID int64, emoji string) error {
109
	var resp Response[bool]
110
	return t.req(ctx, "setMessageReaction", nil, setReactionReq{
111
		ChatID:    chatID,
112
		MessageID: messageID,
113
		Reaction:  []messageReactionReq{{Type: "emoji", Emoji: emoji}},
114
	}, &resp)
115
}
116
117
type tgFile struct {
118
	FilePath string `json:"file_path"`
119
}
120
121
func (t *TelegramSDK) downloadLargestPhoto(ctx context.Context, photos []PhotoSize) ([]byte, string, error) {
122
	if len(photos) == 0 {
123
		return nil, "", nil
124
	}
125
126
	filePath, err := t.getFilePath(ctx, photos[len(photos)-1].FileID)
127
	if err != nil {
128
		return nil, "", err
129
	}
130
131
	fileURL := fmt.Sprintf("%s/file/bot%s/%s", apiBase, t.token, filePath)
132
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileURL, nil)
133
	if err != nil {
134
		return nil, "", err
135
	}
136
137
	res, err := t.client.Do(req)
138
	if err != nil {
139
		return nil, "", err
140
	}
141
	defer res.Body.Close()
142
	if res.StatusCode != http.StatusOK {
143
		return nil, "", fmt.Errorf("photo download failed with status %d", res.StatusCode)
144
	}
145
146
	data, err := io.ReadAll(io.LimitReader(res.Body, maxPhotoBytes+1))
147
	if err != nil {
148
		return nil, "", err
149
	}
150
	if len(data) > maxPhotoBytes {
151
		return nil, "", fmt.Errorf("photo too large: %d bytes", len(data))
152
	}
153
154
	mimeType := http.DetectContentType(data)
155
	if !strings.HasPrefix(mimeType, "image/") {
156
		mimeType = "image/jpeg"
157
	}
158
	return data, mimeType, nil
159
}
160
161
func (t *TelegramSDK) getFilePath(ctx context.Context, fileID string) (string, error) {
162
	params := url.Values{}
163
	params.Set("file_id", fileID)
164
165
	var resp Response[tgFile]
166
	if err := t.req(ctx, "getFile", params, nil, &resp); err != nil {
167
		return "", err
168
	}
169
	return resp.Result.FilePath, nil
170
}
171
172
func (t *TelegramSDK) req(ctx context.Context, method string, params url.Values, body any, out any) error {
173
	u := fmt.Sprintf("%s/bot%s/%s", apiBase, t.token, method)
174
	if params != nil {
175
		u += "?" + params.Encode()
176
	}
177
178
	var req *http.Request
179
	var err error
180
	if body != nil {
181
		var data []byte
182
		data, err = json.Marshal(body)
183
		if err != nil {
184
			return err
185
		}
186
		req, err = http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewReader(data))
187
		if err != nil {
188
			return err
189
		}
190
		req.Header.Set("Content-Type", "application/json")
191
	} else {
192
		req, err = http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
193
		if err != nil {
194
			return err
195
		}
196
	}
197
198
	res, err := t.client.Do(req)
199
	if err != nil {
200
		return err
201
	}
202
	defer res.Body.Close()
203
	return json.NewDecoder(res.Body).Decode(out)
204
}