all repos

rss-tools @ 610059f252252aebddeeed3400f808d3a71c06e4

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