add load more command
This commit is contained in:
@@ -69,7 +69,7 @@ func main() {
|
||||
memorySvc := memory.NewService(selector, store, loc, logger)
|
||||
|
||||
// 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
|
||||
srv := &http.Server{
|
||||
@@ -86,7 +86,7 @@ func main() {
|
||||
|
||||
// Telegram bot
|
||||
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 {
|
||||
logger.Error("failed to create telegram bot", "error", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -118,3 +118,7 @@ timezone = "Europe/Moscow"
|
||||
|
||||
# Уровень логирования: debug, info, warn, error.
|
||||
log_level = "info"
|
||||
|
||||
# Разрешить загрузку дополнительных воспоминаний (кнопка на веб-странице, /more в Telegram).
|
||||
# Полезно для тестирования. Каждое загруженное воспоминание записывается в историю показов.
|
||||
allow_load_more = false
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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: "июня",
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user