all repos

rss-tools @ 7235086123c8b37c0f4db89fb5821d89d0e29094

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