all repos

rss-tools @ d3bc404

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