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
+2 -2
View File
@@ -69,7 +69,7 @@ func main() {
memorySvc := memory.NewService(selector, store, loc, logger) memorySvc := memory.NewService(selector, store, loc, logger)
// Web handler // Web handler
handler := web.NewHandler(memorySvc, cfg.Memos.URL, cfg.Memos.PublicURL, logger) handler := web.NewHandler(memorySvc, cfg.Memos.URL, cfg.Memos.PublicURL, cfg.General.AllowLoadMore, logger)
// HTTP server // HTTP server
srv := &http.Server{ srv := &http.Server{
@@ -86,7 +86,7 @@ func main() {
// Telegram bot // Telegram bot
if cfg.Telegram.Enabled { if cfg.Telegram.Enabled {
tgBot, err := telegram.NewBot(cfg.Telegram, memorySvc, client, cfg.Memos.URL, cfg.Memos.PublicURL, loc, logger) tgBot, err := telegram.NewBot(cfg.Telegram, memorySvc, client, cfg.Memos.URL, cfg.Memos.PublicURL, cfg.General.AllowLoadMore, loc, logger)
if err != nil { if err != nil {
logger.Error("failed to create telegram bot", "error", err) logger.Error("failed to create telegram bot", "error", err)
os.Exit(1) os.Exit(1)
+4
View File
@@ -118,3 +118,7 @@ timezone = "Europe/Moscow"
# Уровень логирования: debug, info, warn, error. # Уровень логирования: debug, info, warn, error.
log_level = "info" log_level = "info"
# Разрешить загрузку дополнительных воспоминаний (кнопка на веб-странице, /more в Telegram).
# Полезно для тестирования. Каждое загруженное воспоминание записывается в историю показов.
allow_load_more = false
+1
View File
@@ -70,6 +70,7 @@ type WebConfig struct {
type GeneralConfig struct { type GeneralConfig struct {
Timezone string `toml:"timezone"` Timezone string `toml:"timezone"`
LogLevel string `toml:"log_level"` LogLevel string `toml:"log_level"`
AllowLoadMore bool `toml:"allow_load_more"`
} }
func Load(path string) (*Config, error) { 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 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
}
+56 -2
View File
@@ -12,6 +12,7 @@ import (
"git.vakhrushev.me/av/remembos/internal/config" "git.vakhrushev.me/av/remembos/internal/config"
"git.vakhrushev.me/av/remembos/internal/memory" "git.vakhrushev.me/av/remembos/internal/memory"
"git.vakhrushev.me/av/remembos/internal/memos" "git.vakhrushev.me/av/remembos/internal/memos"
"git.vakhrushev.me/av/remembos/internal/search"
) )
// Bot sends a daily memory via Telegram. // Bot sends a daily memory via Telegram.
@@ -24,6 +25,7 @@ type Bot struct {
publicURL string publicURL string
loc *time.Location loc *time.Location
logger *slog.Logger logger *slog.Logger
allowLoadMore bool
} }
// NewBot creates a new Telegram bot. // NewBot creates a new Telegram bot.
@@ -32,6 +34,7 @@ func NewBot(
service *memory.Service, service *memory.Service,
client *memos.Client, client *memos.Client,
memosURL, publicURL string, memosURL, publicURL string,
allowLoadMore bool,
loc *time.Location, loc *time.Location,
logger *slog.Logger, logger *slog.Logger,
) (*Bot, error) { ) (*Bot, error) {
@@ -57,11 +60,16 @@ func NewBot(
publicURL: pub, publicURL: pub,
loc: loc, loc: loc,
logger: logger, logger: logger,
allowLoadMore: allowLoadMore,
}, nil }, nil
} }
// Run starts the scheduling loop. It blocks until ctx is cancelled. // Run starts the scheduling loop. It blocks until ctx is cancelled.
func (b *Bot) Run(ctx context.Context) { func (b *Bot) Run(ctx context.Context) {
if b.allowLoadMore {
go b.listenForCommands(ctx)
}
for { for {
next := b.nextSendTime() next := b.nextSendTime()
delay := time.Until(next) 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) b.logger.Error("failed to get today memory", "error", err)
return 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 { if mem == nil {
b.logger.Info("no memory for today, skipping telegram send") b.logger.Info("no memory to send, skipping")
return return
} }
mainText, captionText := formatMemory(mem, b.publicURL) mainText, captionText := formatMemory(mem, b.publicURL)
images := imageAttachments(mem.Memo) images := imageAttachments(mem.Memo)
// Try to download images
var downloaded []imageFile var downloaded []imageFile
if len(images) > 0 { if len(images) > 0 {
downloaded = b.downloadImages(ctx, images) 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 { type imageFile struct {
filename string filename string
data []byte data []byte
+20 -1
View File
@@ -31,6 +31,7 @@ type memoryData struct {
MemoURL string MemoURL string
Tier int Tier int
ShowCount int ShowCount int
AllowLoadMore bool
} }
type errorData struct { type errorData struct {
@@ -43,9 +44,10 @@ type Handler struct {
mux *http.ServeMux mux *http.ServeMux
memosURL string // internal Memos URL (for attachment files) memosURL string // internal Memos URL (for attachment files)
publicURL string // public Memos URL (for memo links) 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 pub := publicURL
if pub == "" { if pub == "" {
pub = memosURL pub = memosURL
@@ -54,11 +56,13 @@ func NewHandler(service *memory.Service, memosURL, publicURL string, logger *slo
service: service, service: service,
memosURL: strings.TrimRight(memosURL, "/"), memosURL: strings.TrimRight(memosURL, "/"),
publicURL: strings.TrimRight(pub, "/"), publicURL: strings.TrimRight(pub, "/"),
allowLoadMore: allowLoadMore,
logger: logger, logger: logger,
mux: http.NewServeMux(), mux: http.NewServeMux(),
} }
h.mux.HandleFunc("GET /", h.handleMemory) h.mux.HandleFunc("GET /", h.handleMemory)
h.mux.HandleFunc("GET /health", h.handleHealth) h.mux.HandleFunc("GET /health", h.handleHealth)
h.mux.HandleFunc("POST /more", h.handleLoadMore)
return h return h
} }
@@ -116,6 +120,7 @@ func (h *Handler) handleMemory(w http.ResponseWriter, r *http.Request) {
MemoURL: memoURL, MemoURL: memoURL,
Tier: mem.Tier, Tier: mem.Tier,
ShowCount: mem.ShowCount, ShowCount: mem.ShowCount,
AllowLoadMore: h.allowLoadMore,
} }
w.Header().Set("Content-Type", "text/html; charset=utf-8") 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") 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{ var months = [...]string{
1: "января", 2: "февраля", 3: "марта", 1: "января", 2: "февраля", 3: "марта",
4: "апреля", 5: "мая", 6: "июня", 4: "апреля", 5: "мая", 6: "июня",
+20
View File
@@ -77,6 +77,21 @@
.meta a:hover { .meta a:hover {
text-decoration: underline; 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> </style>
</head> </head>
<body> <body>
@@ -102,6 +117,11 @@
Tier {{.Tier}} · показов: {{.ShowCount}} Tier {{.Tier}} · показов: {{.ShowCount}}
{{if .MemoURL}} · <a href="{{.MemoURL}}" target="_blank" rel="noopener">оригинал</a>{{end}} {{if .MemoURL}} · <a href="{{.MemoURL}}" target="_blank" rel="noopener">оригинал</a>{{end}}
</div> </div>
{{if .AllowLoadMore}}
<form class="load-more" method="POST" action="/more">
<button type="submit">Другое воспоминание</button>
</form>
{{end}}
</div> </div>
</body> </body>
</html> </html>