all repos

rss-tools @ 20e54e61e32f5ae335cc7c7015d3969c20462437

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: support images, 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", u.Message.Text)
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
	if m.PhotoBase64 == "" {
157
		title := m.Text
158
		if len(title) > 64 {
159
			title = title[:64] + "..."
160
		}
161
		return app.FeedEntry{
162
			Title:   title,
163
			ID:      fmt.Sprintf("telegram-%d", m.MessageID),
164
			Content: m.Text,
165
			Updated: updated,
166
		}
167
	}
168
169
	parts := make([]string, 0, 2)
170
	if text := strings.TrimSpace(m.Text); text != "" {
171
		parts = append(parts, "<p>"+html.EscapeString(text)+"</p>")
172
	}
173
	mimeType := m.PhotoMIMEType
174
	if mimeType == "" {
175
		mimeType = "image/jpeg"
176
	}
177
	parts = append(parts, fmt.Sprintf(`<p><img src="data:%s;base64,%s" alt="telegram image"/></p>`, mimeType, m.PhotoBase64))
178
179
	return app.FeedEntry{
180
		Title:       fmt.Sprintf("🖼️ [%s]", updated.Format("2006-01-02")),
181
		ID:          fmt.Sprintf("telegram-%d", m.MessageID),
182
		Content:     strings.Join(parts, ""),
183
		ContentType: "html",
184
		Updated:     updated,
185
	}
186
}