all repos

rss-tools @ 7235086123c8b37c0f4db89fb5821d89d0e29094

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