all repos

rss-tools @ 6bffb9f

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
will it fix it? idk, 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
	if err := feed.Render(w); err != nil {
66
		http.Error(w, "failed to render feed", http.StatusInternalServerError)
67
		return
68
	}
69
}
70
71
func (t *telegram) worker(ctx context.Context) error {
72
	offset, err := t.loadOffset()
73
	if err != nil {
74
		return err
75
	}
76
77
	for {
78
		updates, err := t.tg.GetUpdates(ctx, offset)
79
		if err != nil {
80
			slog.ErrorContext(ctx, "getUpdates failed", "err", err)
81
			select {
82
			case <-ctx.Done():
83
				return nil
84
			case <-time.After(5 * time.Second):
85
				continue
86
			}
87
		}
88
89
		for _, u := range updates {
90
			if u.Message != nil && u.Message.From != nil {
91
				slog.InfoContext(ctx, "message from", "user_id", u.Message.From.ID, "username", u.Message.From.Username, "msg", messageText(u.Message))
92
			}
93
94
			if u.Message == nil || u.Message.From == nil || u.Message.From.ID != t.allowedID {
95
				offset = u.UpdateID + 1
96
				continue
97
			}
98
99
			if err := t.saveMessage(u.Message); err != nil {
100
				slog.ErrorContext(ctx, "failed to save message", "err", err)
101
			}
102
103
			if err := t.tg.SetReaction(ctx, u.Message.From.ID, u.Message.MessageID, "👍"); err != nil {
104
				slog.ErrorContext(ctx, "failed to set reaction", "err", err)
105
			}
106
107
			offset = u.UpdateID + 1
108
		}
109
110
		if err := t.saveOffset(offset); err != nil {
111
			slog.ErrorContext(ctx, "failed to save offset", "err", err)
112
		}
113
114
		select {
115
		case <-ctx.Done():
116
			return nil
117
		case <-time.After(time.Second):
118
		}
119
	}
120
}
121
122
func (t *telegram) saveOffset(offset int64) error {
123
	return t.db.Set([]byte("offset"), binary.BigEndian.AppendUint64(nil, uint64(offset)))
124
}
125
126
func (t *telegram) loadOffset() (int64, error) {
127
	val, err := t.db.Get([]byte("offset"))
128
	if err != nil || val == nil {
129
		return 0, err
130
	}
131
	return int64(binary.BigEndian.Uint64(val)), nil
132
}
133
134
func (t *telegram) saveMessage(m *Message) error {
135
	var buf bytes.Buffer
136
	if err := gob.NewEncoder(&buf).Encode(m); err != nil {
137
		return err
138
	}
139
	key := binary.BigEndian.AppendUint64(nil, uint64(m.MessageID))
140
	return t.messages.Set(key, buf.Bytes())
141
}
142
143
func (t *telegram) loadMessages() ([]*Message, error) {
144
	var messages []*Message
145
	err := t.messages.ForEach(func(k, v []byte) error {
146
		var m Message
147
		if err := gob.NewDecoder(bytes.NewReader(v)).Decode(&m); err != nil {
148
			return err
149
		}
150
		messages = append(messages, &m)
151
		return nil
152
	})
153
	return messages, err
154
}
155
156
func feedEntryFromMessage(m *Message) app.FeedEntry {
157
	updated := time.Unix(m.Date, 0)
158
	text := messageText(m)
159
	if m.PhotoBase64 == "" {
160
		title := text
161
		if len(title) > 64 {
162
			title = title[:64] + "..."
163
		}
164
		return app.FeedEntry{
165
			Title:   title,
166
			ID:      fmt.Sprintf("telegram-%d", m.MessageID),
167
			Content: text,
168
			Updated: updated,
169
		}
170
	}
171
172
	parts := make([]string, 0, 2)
173
	if t := strings.TrimSpace(text); t != "" {
174
		parts = append(parts, "<p>"+html.EscapeString(t)+"</p>")
175
	}
176
	mimeType := m.PhotoMIMEType
177
	if mimeType == "" {
178
		mimeType = "image/jpeg"
179
	}
180
	parts = append(parts, fmt.Sprintf(`<p><img src="data:%s;base64,%s" alt="telegram image"/></p>`, mimeType, m.PhotoBase64))
181
182
	return app.FeedEntry{
183
		Title:       fmt.Sprintf("🖼️ [%s]", updated.Format("2006-01-02")),
184
		ID:          fmt.Sprintf("telegram-%d", m.MessageID),
185
		Content:     strings.Join(parts, ""),
186
		ContentType: "html",
187
		Updated:     updated,
188
	}
189
}
190
191
func messageText(m *Message) string {
192
	if m == nil {
193
		return ""
194
	}
195
	if caption := strings.TrimSpace(m.Caption); caption != "" {
196
		return m.Caption
197
	}
198
	return m.Text
199
}