add load more command

This commit is contained in:
2026-02-12 17:52:32 +03:00
parent 4e1d1ca35f
commit a058b622e3
7 changed files with 161 additions and 33 deletions
+3 -2
View File
@@ -68,8 +68,9 @@ type WebConfig struct {
}
type GeneralConfig struct {
Timezone string `toml:"timezone"`
LogLevel string `toml:"log_level"`
Timezone string `toml:"timezone"`
LogLevel string `toml:"log_level"`
AllowLoadMore bool `toml:"allow_load_more"`
}
func Load(path string) (*Config, error) {
+30
View File
@@ -68,3 +68,33 @@ func (s *Service) GetTodayMemory(ctx context.Context) (*search.Memory, error) {
return mem, nil
}
// LoadNewMemory selects a new memory ignoring the cache, records the show,
// and updates the cache so that subsequent GetTodayMemory calls return it.
func (s *Service) LoadNewMemory(ctx context.Context) (*search.Memory, error) {
today := time.Now().In(s.loc)
dayKey := today.Format("2006-01-02")
s.logger.Info("loading additional memory", "date", dayKey)
mem, err := s.selector.Select(ctx, today)
if err != nil {
return nil, err
}
if mem == nil {
s.logger.Warn("no memory found for load more")
return nil, nil
}
if err := s.store.RecordShow(ctx, mem.Memo.Name, mem.Tier); err != nil {
s.logger.Error("failed to record show", "error", err)
}
s.mu.Lock()
s.cacheDay = dayKey
s.cached = mem
s.mu.Unlock()
return mem, nil
}
+72 -18
View File
@@ -12,18 +12,20 @@ import (
"git.vakhrushev.me/av/remembos/internal/config"
"git.vakhrushev.me/av/remembos/internal/memory"
"git.vakhrushev.me/av/remembos/internal/memos"
"git.vakhrushev.me/av/remembos/internal/search"
)
// Bot sends a daily memory via Telegram.
type Bot struct {
api *tgbotapi.BotAPI
service *memory.Service
client *memos.Client
chatID int64
sendAt string // "HH:MM"
publicURL string
loc *time.Location
logger *slog.Logger
api *tgbotapi.BotAPI
service *memory.Service
client *memos.Client
chatID int64
sendAt string // "HH:MM"
publicURL string
loc *time.Location
logger *slog.Logger
allowLoadMore bool
}
// NewBot creates a new Telegram bot.
@@ -32,6 +34,7 @@ func NewBot(
service *memory.Service,
client *memos.Client,
memosURL, publicURL string,
allowLoadMore bool,
loc *time.Location,
logger *slog.Logger,
) (*Bot, error) {
@@ -49,19 +52,24 @@ func NewBot(
logger.Info("telegram bot authorized", "username", api.Self.UserName)
return &Bot{
api: api,
service: service,
client: client,
chatID: cfg.ChatID,
sendAt: cfg.SendAt,
publicURL: pub,
loc: loc,
logger: logger,
api: api,
service: service,
client: client,
chatID: cfg.ChatID,
sendAt: cfg.SendAt,
publicURL: pub,
loc: loc,
logger: logger,
allowLoadMore: allowLoadMore,
}, nil
}
// Run starts the scheduling loop. It blocks until ctx is cancelled.
func (b *Bot) Run(ctx context.Context) {
if b.allowLoadMore {
go b.listenForCommands(ctx)
}
for {
next := b.nextSendTime()
delay := time.Until(next)
@@ -105,15 +113,20 @@ func (b *Bot) sendDaily(ctx context.Context) {
b.logger.Error("failed to get today memory", "error", err)
return
}
b.sendMemory(ctx, mem)
}
// sendMemory formats and sends a memory via Telegram.
func (b *Bot) sendMemory(ctx context.Context, mem *search.Memory) {
if mem == nil {
b.logger.Info("no memory for today, skipping telegram send")
b.logger.Info("no memory to send, skipping")
return
}
mainText, captionText := formatMemory(mem, b.publicURL)
images := imageAttachments(mem.Memo)
// Try to download images
var downloaded []imageFile
if len(images) > 0 {
downloaded = b.downloadImages(ctx, images)
@@ -124,6 +137,47 @@ func (b *Bot) sendDaily(ctx context.Context) {
}
}
// listenForCommands polls for Telegram updates and handles /more commands.
func (b *Bot) listenForCommands(ctx context.Context) {
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := b.api.GetUpdatesChan(u)
for {
select {
case <-ctx.Done():
return
case update, ok := <-updates:
if !ok {
return
}
if update.Message == nil || !update.Message.IsCommand() {
continue
}
if update.Message.Chat.ID != b.chatID {
continue
}
if update.Message.Command() == "more" {
b.handleMore(ctx)
}
}
}
}
// handleMore loads a new memory and sends it.
func (b *Bot) handleMore(ctx context.Context) {
b.logger.Info("handling /more command")
mem, err := b.service.LoadNewMemory(ctx)
if err != nil {
b.logger.Error("failed to load new memory", "error", err)
return
}
b.sendMemory(ctx, mem)
}
type imageFile struct {
filename string
data []byte
+30 -11
View File
@@ -31,6 +31,7 @@ type memoryData struct {
MemoURL string
Tier int
ShowCount int
AllowLoadMore bool
}
type errorData struct {
@@ -38,27 +39,30 @@ type errorData struct {
}
type Handler struct {
service *memory.Service
logger *slog.Logger
mux *http.ServeMux
memosURL string // internal Memos URL (for attachment files)
publicURL string // public Memos URL (for memo links)
service *memory.Service
logger *slog.Logger
mux *http.ServeMux
memosURL string // internal Memos URL (for attachment files)
publicURL string // public Memos URL (for memo links)
allowLoadMore bool
}
func NewHandler(service *memory.Service, memosURL, publicURL string, logger *slog.Logger) *Handler {
func NewHandler(service *memory.Service, memosURL, publicURL string, allowLoadMore bool, logger *slog.Logger) *Handler {
pub := publicURL
if pub == "" {
pub = memosURL
}
h := &Handler{
service: service,
memosURL: strings.TrimRight(memosURL, "/"),
publicURL: strings.TrimRight(pub, "/"),
logger: logger,
mux: http.NewServeMux(),
service: service,
memosURL: strings.TrimRight(memosURL, "/"),
publicURL: strings.TrimRight(pub, "/"),
allowLoadMore: allowLoadMore,
logger: logger,
mux: http.NewServeMux(),
}
h.mux.HandleFunc("GET /", h.handleMemory)
h.mux.HandleFunc("GET /health", h.handleHealth)
h.mux.HandleFunc("POST /more", h.handleLoadMore)
return h
}
@@ -116,6 +120,7 @@ func (h *Handler) handleMemory(w http.ResponseWriter, r *http.Request) {
MemoURL: memoURL,
Tier: mem.Tier,
ShowCount: mem.ShowCount,
AllowLoadMore: h.allowLoadMore,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -129,6 +134,20 @@ func (h *Handler) handleHealth(w http.ResponseWriter, _ *http.Request) {
fmt.Fprint(w, "ok")
}
func (h *Handler) handleLoadMore(w http.ResponseWriter, r *http.Request) {
if !h.allowLoadMore {
http.Error(w, "load more is disabled", http.StatusForbidden)
return
}
_, err := h.service.LoadNewMemory(r.Context())
if err != nil {
h.logger.Error("failed to load new memory", "error", err)
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}
var months = [...]string{
1: "января", 2: "февраля", 3: "марта",
4: "апреля", 5: "мая", 6: "июня",
+20
View File
@@ -77,6 +77,21 @@
.meta a:hover {
text-decoration: underline;
}
.load-more {
margin-top: 1rem;
}
.load-more button {
background: #e8f0fe;
color: #1a73e8;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.9rem;
cursor: pointer;
}
.load-more button:hover {
background: #d2e3fc;
}
</style>
</head>
<body>
@@ -102,6 +117,11 @@
Tier {{.Tier}} · показов: {{.ShowCount}}
{{if .MemoURL}} · <a href="{{.MemoURL}}" target="_blank" rel="noopener">оригинал</a>{{end}}
</div>
{{if .AllowLoadMore}}
<form class="load-more" method="POST" action="/more">
<button type="submit">Другое воспоминание</button>
</form>
{{end}}
</div>
</body>
</html>