2 Commits

Author SHA1 Message Date
av 2ca838a50f remove tags from telegram post
release / docker-image (push) Successful in 1m21s
release / goreleaser (push) Successful in 10m20s
2026-02-22 12:25:38 +03:00
av 2c6e71bad5 fix today memory after restart
release / docker-image (push) Successful in 1m8s
release / goreleaser (push) Successful in 10m14s
2026-02-13 09:58:37 +03:00
5 changed files with 83 additions and 13 deletions
+1 -1
View File
@@ -66,7 +66,7 @@ func main() {
selector := search.NewSelector(client, store, &cfg.Search, loc, logger) selector := search.NewSelector(client, store, &cfg.Search, loc, logger)
// Memory service // Memory service
memorySvc := memory.NewService(selector, store, loc, logger) memorySvc := memory.NewService(selector, store, client, loc, logger)
// Web handler // Web handler
handler := web.NewHandler(memorySvc, cfg.Memos.URL, cfg.Memos.PublicURL, cfg.General.AllowLoadMore, logger) handler := web.NewHandler(memorySvc, cfg.Memos.URL, cfg.Memos.PublicURL, cfg.General.AllowLoadMore, logger)
+37 -2
View File
@@ -6,6 +6,7 @@ import (
"sync" "sync"
"time" "time"
"git.vakhrushev.me/av/remembos/internal/memos"
"git.vakhrushev.me/av/remembos/internal/search" "git.vakhrushev.me/av/remembos/internal/search"
"git.vakhrushev.me/av/remembos/internal/storage" "git.vakhrushev.me/av/remembos/internal/storage"
) )
@@ -14,6 +15,7 @@ import (
type Service struct { type Service struct {
selector *search.Selector selector *search.Selector
store *storage.Storage store *storage.Storage
client *memos.Client
loc *time.Location loc *time.Location
logger *slog.Logger logger *slog.Logger
@@ -22,16 +24,18 @@ type Service struct {
cached *search.Memory cached *search.Memory
} }
func NewService(selector *search.Selector, store *storage.Storage, loc *time.Location, logger *slog.Logger) *Service { func NewService(selector *search.Selector, store *storage.Storage, client *memos.Client, loc *time.Location, logger *slog.Logger) *Service {
return &Service{ return &Service{
selector: selector, selector: selector,
store: store, store: store,
client: client,
loc: loc, loc: loc,
logger: logger, logger: logger,
} }
} }
// GetTodayMemory returns the memory for today, caching the result. // GetTodayMemory returns the memory for today, caching the result.
// On cache miss (e.g. after restart), it checks the DB for a memo already shown today.
func (s *Service) GetTodayMemory(ctx context.Context) (*search.Memory, error) { func (s *Service) GetTodayMemory(ctx context.Context) (*search.Memory, error) {
today := time.Now().In(s.loc) today := time.Now().In(s.loc)
dayKey := today.Format("2006-01-02") dayKey := today.Format("2006-01-02")
@@ -44,6 +48,38 @@ func (s *Service) GetTodayMemory(ctx context.Context) (*search.Memory, error) {
} }
s.mu.Unlock() s.mu.Unlock()
// Try to restore from DB — check if a memo was already shown today
dayStart := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, s.loc)
dayEnd := dayStart.AddDate(0, 0, 1)
memoName, tier, err := s.store.GetLastShownToday(ctx, dayStart.Unix(), dayEnd.Unix())
if err != nil {
s.logger.Error("failed to check today's show history", "error", err)
// Fall through to select a new one
}
if memoName != "" {
s.logger.Info("restoring today's memory from history", "memo", memoName)
memo, err := s.client.GetMemo(ctx, memoName)
if err != nil {
s.logger.Error("failed to fetch memo from history, selecting new", "memo", memoName, "error", err)
} else {
mem := &search.Memory{
Memo: memo,
Tier: tier,
Date: memo.DisplayTime,
}
s.mu.Lock()
s.cacheDay = dayKey
s.cached = mem
s.mu.Unlock()
return mem, nil
}
}
s.logger.Info("selecting new memory", "date", dayKey) s.logger.Info("selecting new memory", "date", dayKey)
mem, err := s.selector.Select(ctx, today) mem, err := s.selector.Select(ctx, today)
@@ -58,7 +94,6 @@ func (s *Service) GetTodayMemory(ctx context.Context) (*search.Memory, error) {
if err := s.store.RecordShow(ctx, mem.Memo.Name, mem.Tier); err != nil { if err := s.store.RecordShow(ctx, mem.Memo.Name, mem.Tier); err != nil {
s.logger.Error("failed to record show", "error", err) s.logger.Error("failed to record show", "error", err)
// Non-fatal: still return the memory
} }
s.mu.Lock() s.mu.Lock()
+28
View File
@@ -67,6 +67,34 @@ func (c *Client) ListMemos(ctx context.Context, filter string, pageSize int, pag
return &result, nil return &result, nil
} }
// GetMemo fetches a single memo by its resource name (e.g. "memos/123").
func (c *Client) GetMemo(ctx context.Context, name string) (*Memo, error) {
reqURL := fmt.Sprintf("%s/api/v1/%s", c.baseURL, name)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, http.NoBody)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.token)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("do request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("memos API returned %d: %s", resp.StatusCode, body)
}
var memo Memo
if err := json.NewDecoder(resp.Body).Decode(&memo); err != nil {
return nil, fmt.Errorf("decode memo: %w", err)
}
return &memo, nil
}
// DownloadAttachment downloads the attachment data as bytes. // DownloadAttachment downloads the attachment data as bytes.
func (c *Client) DownloadAttachment(ctx context.Context, att Attachment) ([]byte, error) { func (c *Client) DownloadAttachment(ctx context.Context, att Attachment) ([]byte, error) {
var reqURL string var reqURL string
+17
View File
@@ -2,6 +2,8 @@ package storage
import ( import (
"context" "context"
"database/sql"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@@ -65,6 +67,21 @@ func (s *Storage) GetShowCounts(ctx context.Context, memoNames []string) (map[st
return result, rows.Err() return result, rows.Err()
} }
// GetLastShownToday returns the memo_name and tier of the most recently shown memo
// within the given time range [from, to). Returns empty string if nothing was shown.
func (s *Storage) GetLastShownToday(ctx context.Context, from, to int64) (memoName string, tier int, err error) {
err = s.db.QueryRowContext(ctx,
`SELECT memo_name, tier FROM show_history WHERE shown_at >= ? AND shown_at < ? ORDER BY shown_at DESC LIMIT 1`,
from, to).Scan(&memoName, &tier)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return "", 0, nil
}
return "", 0, fmt.Errorf("get last shown today: %w", err)
}
return memoName, tier, nil
}
// RecordShow records that a memo was shown. // RecordShow records that a memo was shown.
func (s *Storage) RecordShow(ctx context.Context, memoName string, tier int) error { func (s *Storage) RecordShow(ctx context.Context, memoName string, tier int) error {
_, err := s.db.ExecContext(ctx, _, err := s.db.ExecContext(ctx,
-10
View File
@@ -29,16 +29,6 @@ func formatMemory(mem *search.Memory, publicURL string) string {
// Content // Content
b.WriteString(escapeHTML(mem.Memo.Content)) b.WriteString(escapeHTML(mem.Memo.Content))
// Tags
if len(mem.Memo.Tags) > 0 {
b.WriteString("\n\n")
tags := make([]string, len(mem.Memo.Tags))
for i, t := range mem.Memo.Tags {
tags[i] = "#" + t
}
b.WriteString(strings.Join(tags, " "))
}
// Link to original // Link to original
memoURL := fmt.Sprintf("%s/%s", publicURL, mem.Memo.Name) memoURL := fmt.Sprintf("%s/%s", publicURL, mem.Memo.Name)
b.WriteString("\n\n<a href=\"" + memoURL + "\">Оригинал</a>") b.WriteString("\n\n<a href=\"" + memoURL + "\">Оригинал</a>")