all repos

rss-tools @ ecbe2c093170b8ecb436d4951ce9d0f4ee781334

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

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
reuse app.logger, 1 month ago
1
package telegram
2
3
import (
4
	"bytes"
5
	"context"
6
	"encoding/binary"
7
	"encoding/gob"
8
	"fmt"
9
	"log/slog"
10
	"net/http"
11
	"strings"
12
	"time"
13
14
	"olexsmir.xyz/rss-tools/app"
15
)
16
17
type telegram struct {
18
	db        *app.Bucket
19
	messages  *app.Bucket
20
	client    *http.Client
21
	tg        *TelegramSDK
22
	allowedID int64
23
	logger *slog.Logger
24
}
25
26
func Register(a *app.App) error {
27
	db, err := a.Bucket("telegram")
28
	if err != nil {
29
		return err
30
	}
31
32
	messages, err := a.Bucket("telegram:messages")
33
	if err != nil {
34
		return err
35
	}
36
37
	t := &telegram{
38
		db:        db,
39
		messages:  messages,
40
		client:    a.Client,
41
		tg:        NewSDK(a.Client, a.Config.TGToken),
42
		allowedID: a.Config.TGUserID,
43
		logger: a.Logger,
44
	}
45
46
	a.AddWorker(t.worker)
47
	a.Route("GET /telegram", t.handler)
48
	return nil
49
}
50
51
func (t *telegram) handler(w http.ResponseWriter, r *http.Request) {
52
	// todo: cache feed contruction
53
	// todo: dont include messages older than N days
54
55
	messages, err := t.loadMessages()
56
	if err != nil {
57
		http.Error(w, "failed to load messages", http.StatusInternalServerError)
58
		return
59
	}
60
61
	feed := app.NewFeed("Telegram feed", "telegram-feed")
62
	for _, m := range messages {
63
		feed.Add(feedEntryFromMessage(m))
64
	}
65
66
	if err := feed.Render(w); err != nil {
67
		http.Error(w, "failed to render feed", http.StatusInternalServerError)
68
		return
69
	}
70
}
71
72
func (t *telegram) worker(ctx context.Context) error {
73
	offset, err := t.loadOffset()
74
	if err != nil {
75
		return err
76
	}
77
78
	for {
79
		updates, err := t.tg.GetUpdates(ctx, offset)
80
		if err != nil {
81
			t.logger.ErrorContext(ctx, "getUpdates failed", "err", err)
82
			select {
83
			case <-ctx.Done():
84
				return nil
85
			case <-time.After(5 * time.Second):
86
				continue
87
			}
88
		}
89
90
		for _, u := range updates {
91
			if u.Message != nil && u.Message.From != nil {
92
				t.logger.InfoContext(ctx, "message from", "user_id", u.Message.From.ID, "username", u.Message.From.Username, "msg", messageText(u.Message))
93
			}
94
95
			if u.Message == nil || u.Message.From == nil || u.Message.From.ID != t.allowedID {
96
				offset = u.UpdateID + 1
97
				continue
98
			}
99
100
			if err := t.saveMessage(u.Message); err != nil {
101
				t.logger.ErrorContext(ctx, "failed to save message", "err", err)
102
			}
103
104
			if err := t.tg.SetReaction(ctx, u.Message.From.ID, u.Message.MessageID, "👍"); err != nil {
105
				slog.ErrorContext(ctx, "failed to set reaction", "err", err)
106
			}
107
108
			offset = u.UpdateID + 1
109
		}
110
111
		if err := t.saveOffset(offset); err != nil {
112
			slog.ErrorContext(ctx, "failed to save offset", "err", err)
113
		}
114
115
		select {
116
		case <-ctx.Done():
117
			return nil
118
		case <-time.After(time.Second):
119
		}
120
	}
121
}
122
123
func (t *telegram) saveOffset(offset int64) error {
124
	return t.db.Set([]byte("offset"), binary.BigEndian.AppendUint64(nil, uint64(offset)))
125
}
126
127
func (t *telegram) loadOffset() (int64, error) {
128
	val, err := t.db.Get([]byte("offset"))
129
	if err != nil || val == nil {
130
		return 0, err
131
	}
132
	return int64(binary.BigEndian.Uint64(val)), nil
133
}
134
135
func (t *telegram) saveMessage(m *Message) error {
136
	var buf bytes.Buffer
137
	if err := gob.NewEncoder(&buf).Encode(m); err != nil {
138
		return err
139
	}
140
	key := binary.BigEndian.AppendUint64(nil, uint64(m.MessageID))
141
	return t.messages.Set(key, buf.Bytes())
142
}
143
144
func (t *telegram) loadMessages() ([]*Message, error) {
145
	var messages []*Message
146
	err := t.messages.ForEach(func(k, v []byte) error {
147
		var m Message
148
		if err := gob.NewDecoder(bytes.NewReader(v)).Decode(&m); err != nil {
149
			return err
150
		}
151
		messages = append(messages, &m)
152
		return nil
153
	})
154
	return messages, err
155
}
156
157
func feedEntryFromMessage(m *Message) app.FeedEntry {
158
	updated := time.Unix(m.Date, 0)
159
	text := messageText(m)
160
	normalizedLinks := normalizeLinks(messageLinks(text))
161
	entryID := fmt.Sprintf("telegram-%d", m.MessageID)
162
	if videoID, ok := firstYouTubeVideoID(normalizedLinks); ok {
163
		entryID = "yt:video:" + videoID
164
	}
165
166
	if m.PhotoBase64 == "" {
167
		title := text
168
		if len(title) > 64 {
169
			title = title[:64] + "..."
170
		}
171
172
		content := text
173
		contentType := ""
174
		if len(normalizedLinks) > 0 {
175
			content, _ = linkifyMessageText(text)
176
			contentType = "html"
177
		}
178
179
		return app.FeedEntry{
180
			Title:       title,
181
			ID:          entryID,
182
			Links:       feedLinks(normalizedLinks),
183
			Content:     content,
184
			Updated:     updated,
185
			ContentType: contentType,
186
		}
187
	}
188
189
	parts := make([]string, 0, 2)
190
	if t := strings.TrimSpace(text); t != "" {
191
		linkified, _ := linkifyMessageText(t)
192
		parts = append(parts, "<p>"+linkified+"</p>")
193
	}
194
	mimeType := m.PhotoMIMEType
195
	if mimeType == "" {
196
		mimeType = "image/jpeg"
197
	}
198
	parts = append(parts, fmt.Sprintf(`<p><img src="data:%s;base64,%s" alt="telegram image"/></p>`, mimeType, m.PhotoBase64))
199
200
	return app.FeedEntry{
201
		Title:       fmt.Sprintf("🖼️ [%s]", updated.Format("2006-01-02")),
202
		ID:          entryID,
203
		Links:       feedLinks(normalizedLinks),
204
		Content:     strings.Join(parts, ""),
205
		ContentType: "html",
206
		Updated:     updated,
207
	}
208
}
209
210
func messageText(m *Message) string {
211
	if m == nil {
212
		return ""
213
	}
214
	if caption := strings.TrimSpace(m.Caption); caption != "" {
215
		return m.Caption
216
	}
217
	return m.Text
218
}