diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..b59dc44
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,34 @@
+linters:
+ enable:
+ - bodyclose
+ - errcheck
+ - errorlint
+ - goconst
+ - gocritic
+ - gofmt
+ - gosec
+ - gosimple
+ - govet
+ - ineffassign
+ - misspell
+ - noctx
+ - prealloc
+ - revive
+ - staticcheck
+ - typecheck
+ - unconvert
+ - unparam
+ - unused
+ - wastedassign
+
+linters-settings:
+ gocritic:
+ enabled-tags:
+ - diagnostic
+ - style
+ - performance
+ revive:
+ rules:
+ - name: blank-imports
+ - name: exported
+ disabled: true
diff --git a/cmd/remembos/main.go b/cmd/remembos/main.go
index d098272..4df63f0 100644
--- a/cmd/remembos/main.go
+++ b/cmd/remembos/main.go
@@ -63,7 +63,7 @@ func main() {
client := memos.NewClient(cfg.Memos.URL, cfg.Memos.Token)
// Search selector
- selector := search.NewSelector(client, store, cfg.Search, loc, logger)
+ selector := search.NewSelector(client, store, &cfg.Search, loc, logger)
// Memory service
memorySvc := memory.NewService(selector, store, loc, logger)
@@ -71,6 +71,18 @@ func main() {
// Web handler
handler := web.NewHandler(memorySvc, cfg.Memos.URL, cfg.Memos.PublicURL, cfg.General.AllowLoadMore, logger)
+ // Telegram bot
+ var tgBot *telegram.Bot
+ if cfg.Telegram.Enabled {
+ var err error
+ 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)
+ store.Close()
+ os.Exit(1) //nolint:gocritic // store.Close() called above; linter doesn't track manual cleanup
+ }
+ }
+
// HTTP server
srv := &http.Server{
Addr: cfg.Web.Listen,
@@ -84,13 +96,7 @@ func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
- // Telegram bot
- if cfg.Telegram.Enabled {
- 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)
- }
+ if tgBot != nil {
go tgBot.Run(ctx)
}
diff --git a/internal/memos/client.go b/internal/memos/client.go
index d03225b..f48d9db 100644
--- a/internal/memos/client.go
+++ b/internal/memos/client.go
@@ -43,7 +43,7 @@ func (c *Client) ListMemos(ctx context.Context, filter string, pageSize int, pag
}
u.RawQuery = q.Encode()
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), http.NoBody)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
@@ -79,7 +79,7 @@ func (c *Client) DownloadAttachment(ctx context.Context, att Attachment) ([]byte
needsAuth = true
}
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, http.NoBody)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
diff --git a/internal/search/scoring.go b/internal/search/scoring.go
index 440ec2b..33bb567 100644
--- a/internal/search/scoring.go
+++ b/internal/search/scoring.go
@@ -54,7 +54,7 @@ func weightedSelect(candidates []candidate, preferOlder bool, maxYearsBack int)
total += score
}
- r := rand.Float64() * total
+ r := rand.Float64() * total //nolint:gosec // non-cryptographic use
var cumulative float64
for i, s := range scores {
cumulative += s
diff --git a/internal/search/selector.go b/internal/search/selector.go
index 7995779..df19f1d 100644
--- a/internal/search/selector.go
+++ b/internal/search/selector.go
@@ -22,19 +22,16 @@ type Selector struct {
logger *slog.Logger
}
-func NewSelector(client *memos.Client, store *storage.Storage, cfg config.SearchConfig, loc *time.Location, logger *slog.Logger) *Selector {
+func NewSelector(client *memos.Client, store *storage.Storage, cfg *config.SearchConfig, loc *time.Location, logger *slog.Logger) *Selector {
return &Selector{
client: client,
store: store,
- cfg: cfg,
+ cfg: *cfg,
loc: loc,
logger: logger,
}
}
-// tierFunc returns date ranges for a given tier.
-type tierFunc func(today time.Time, maxYears int, loc *time.Location) []DateRange
-
// Select runs the full search algorithm and returns one Memory for the given day.
func (s *Selector) Select(ctx context.Context, today time.Time) (*Memory, error) {
weights := s.cfg.TierWeights.AsSlice()
@@ -248,7 +245,7 @@ func weightedTierOrder(weights [7]int) []int {
break
}
- r := rand.IntN(totalWeight)
+ r := rand.IntN(totalWeight) //nolint:gosec // non-cryptographic use
cumulative := 0
for i, e := range remaining {
cumulative += e.weight
diff --git a/internal/storage/history.go b/internal/storage/history.go
index f1b5e1d..8d0cac0 100644
--- a/internal/storage/history.go
+++ b/internal/storage/history.go
@@ -43,7 +43,7 @@ func (s *Storage) GetShowCounts(ctx context.Context, memoNames []string) (map[st
args[i] = name
}
- query := fmt.Sprintf(
+ query := fmt.Sprintf( //nolint:gosec // placeholders are always "?"
`SELECT memo_name, COUNT(*) FROM show_history WHERE memo_name IN (%s) GROUP BY memo_name`,
strings.Join(placeholders, ","))
diff --git a/internal/storage/storage.go b/internal/storage/storage.go
index b24f92b..0e5c302 100644
--- a/internal/storage/storage.go
+++ b/internal/storage/storage.go
@@ -5,7 +5,7 @@ import (
"database/sql"
"fmt"
- _ "modernc.org/sqlite"
+ _ "modernc.org/sqlite" // SQLite driver
)
type Storage struct {
diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go
index 4d2ae99..16dbd61 100644
--- a/internal/telegram/bot.go
+++ b/internal/telegram/bot.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log/slog"
+ "strconv"
"strings"
"time"
@@ -93,8 +94,12 @@ func (b *Bot) nextSendTime() time.Time {
hour := 9
minute := 0
if len(parts) == 2 {
- fmt.Sscanf(parts[0], "%d", &hour)
- fmt.Sscanf(parts[1], "%d", &minute)
+ if h, err := strconv.Atoi(parts[0]); err == nil {
+ hour = h
+ }
+ if m, err := strconv.Atoi(parts[1]); err == nil {
+ minute = m
+ }
}
target := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, b.loc)
@@ -185,7 +190,7 @@ type imageFile struct {
// downloadImages downloads image attachments, skipping failures.
func (b *Bot) downloadImages(ctx context.Context, attachments []memos.Attachment) []imageFile {
- var files []imageFile
+ files := make([]imageFile, 0, len(attachments))
for _, att := range attachments {
data, err := b.client.DownloadAttachment(ctx, att)
if err != nil {
diff --git a/internal/telegram/format.go b/internal/telegram/format.go
index 75023e5..cc029cd 100644
--- a/internal/telegram/format.go
+++ b/internal/telegram/format.go
@@ -41,7 +41,7 @@ func formatMemory(mem *search.Memory, publicURL string) (mainText, captionText s
// Link to original
memoURL := fmt.Sprintf("%s/%s", publicURL, mem.Memo.Name)
- b.WriteString(fmt.Sprintf("\n\nОригинал", memoURL))
+ b.WriteString("\n\nОригинал")
full := b.String()
mainText = truncateHTML(full, maxMessageLen)
diff --git a/internal/web/handler.go b/internal/web/handler.go
index bf7c171..04a4c06 100644
--- a/internal/web/handler.go
+++ b/internal/web/handler.go
@@ -80,21 +80,25 @@ func (h *Handler) handleMemory(w http.ResponseWriter, r *http.Request) {
if err != nil {
h.logger.Error("failed to get memory", "error", err)
w.WriteHeader(http.StatusInternalServerError)
- templates.ExecuteTemplate(w, "error.html", errorData{
+ if err := templates.ExecuteTemplate(w, "error.html", errorData{
Message: "Не удалось загрузить воспоминание",
- })
+ }); err != nil {
+ h.logger.Error("template render failed", "error", err)
+ }
return
}
if mem == nil {
w.WriteHeader(http.StatusOK)
- templates.ExecuteTemplate(w, "error.html", errorData{
+ if err := templates.ExecuteTemplate(w, "error.html", errorData{
Message: "Нет заметок для воспоминания",
- })
+ }); err != nil {
+ h.logger.Error("template render failed", "error", err)
+ }
return
}
- var images []imageData
+ images := make([]imageData, 0, len(mem.Memo.Attachments))
for _, att := range mem.Memo.Attachments {
if !att.IsImage() {
continue