add load more command
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ 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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
+72
-18
@@ -12,18 +12,20 @@ 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.
|
||||||
type Bot struct {
|
type Bot struct {
|
||||||
api *tgbotapi.BotAPI
|
api *tgbotapi.BotAPI
|
||||||
service *memory.Service
|
service *memory.Service
|
||||||
client *memos.Client
|
client *memos.Client
|
||||||
chatID int64
|
chatID int64
|
||||||
sendAt string // "HH:MM"
|
sendAt string // "HH:MM"
|
||||||
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) {
|
||||||
@@ -49,19 +52,24 @@ func NewBot(
|
|||||||
logger.Info("telegram bot authorized", "username", api.Self.UserName)
|
logger.Info("telegram bot authorized", "username", api.Self.UserName)
|
||||||
|
|
||||||
return &Bot{
|
return &Bot{
|
||||||
api: api,
|
api: api,
|
||||||
service: service,
|
service: service,
|
||||||
client: client,
|
client: client,
|
||||||
chatID: cfg.ChatID,
|
chatID: cfg.ChatID,
|
||||||
sendAt: cfg.SendAt,
|
sendAt: cfg.SendAt,
|
||||||
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
|
||||||
|
|||||||
+30
-11
@@ -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 {
|
||||||
@@ -38,27 +39,30 @@ type errorData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
service *memory.Service
|
service *memory.Service
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
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
|
||||||
}
|
}
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
service: service,
|
service: service,
|
||||||
memosURL: strings.TrimRight(memosURL, "/"),
|
memosURL: strings.TrimRight(memosURL, "/"),
|
||||||
publicURL: strings.TrimRight(pub, "/"),
|
publicURL: strings.TrimRight(pub, "/"),
|
||||||
logger: logger,
|
allowLoadMore: allowLoadMore,
|
||||||
mux: http.NewServeMux(),
|
logger: logger,
|
||||||
|
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: "июня",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user