Files
worktimeBot/cmd/bot/main.go
db123 d15ed46066 fix: resolve go vet error and dayStart logic in ComputeDailyTotals
- Fix 'string(d)' int-to-rune conversion in rpg_test.go by using fmt.Sprintf
- Fix ComputeDailyTotals dayStart logic: initial state should be StateWorking
  when dayStart > 0, correctly counting work from dayStart to first out event
- Update README project structure to reflect new domain/handler/repo layout
- Implement missing achievements: rpg_pioneer (on RPG enable) and
  salary_setter (on first rate set)
- Add tryUnlockAchievement helper to Handler
2026-06-25 09:54:22 +03:30

164 lines
3.8 KiB
Go

package main
import (
"context"
"log/slog"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
tgbot "github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/joho/godotenv"
"worktimeBot/internal/handler"
"worktimeBot/internal/repo"
)
func main() {
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelInfo,
})))
godotenv.Load()
token := os.Getenv("BOT_TOKEN")
dbPath := getEnvDefault("DB_PATH", "db.sqlite3")
store, err := repo.NewStore(dbPath)
if err != nil {
slog.Error("failed to open database", "path", dbPath, "error", err)
os.Exit(1)
}
allowedUsers := parseAllowedUsers(os.Getenv("BOT_ALLOWED_USERS"))
var h *handler.Handler
dispatch := func(ctx context.Context, b *tgbot.Bot, update *models.Update) {
if update.Message != nil {
h.HandleMessage(ctx, update.Message)
}
if update.CallbackQuery != nil {
h.HandleCallback(ctx, update.CallbackQuery)
}
}
b, err := tgbot.New(token, tgbot.WithDefaultHandler(dispatch))
if err != nil {
slog.Error("failed to create bot", "error", err)
os.Exit(1)
}
h = handler.NewHandler(b, store, allowedUsers)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
if url := os.Getenv("BOT_WEBHOOK_URL"); url != "" {
startWebhook(ctx, b, h, url)
} else {
if _, err := b.DeleteWebhook(ctx, &tgbot.DeleteWebhookParams{}); err != nil {
slog.Warn("failed to delete webhook", "error", err)
}
go dailyReportScheduler(ctx, h, store)
slog.Info("bot started",
"allowed_users", len(allowedUsers),
"mode", "polling",
)
b.Start(ctx)
}
store.Close()
}
// dailyReportScheduler runs a periodic ticker that triggers daily report checks every 30 seconds.
func dailyReportScheduler(ctx context.Context, h *handler.Handler, store repo.Repository) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
checkDailyReports(ctx, h, store)
}
}
}
// checkDailyReports iterates all users and sends a daily report to those whose report time has arrived.
func checkDailyReports(ctx context.Context, h *handler.Handler, store repo.Repository) {
users, err := store.GetAllUsers()
if err != nil {
slog.Error("report scheduler: get users", "error", err)
return
}
now := time.Now().UTC()
for _, u := range users {
if !u.IsActive {
continue
}
st, err := store.GetOrCreateSettings(u.ID)
if err != nil || !st.ReportEnabled {
continue
}
loc, err := time.LoadLocation(u.Timezone)
if err != nil {
slog.Error("invalid timezone for user, skipping report", "chat_id", u.ChatID, "timezone", u.Timezone, "error", err)
continue
}
localNow := now.In(loc)
today := localNow.Format("2006-01-02")
currentHHMM := localNow.Format("15:04")
if currentHHMM != st.ReportTime {
continue
}
if st.LastReportDate == today {
continue
}
slog.Info("sending daily report",
"user_id", u.ID, "chat_id", u.ChatID, "timezone", u.Timezone,
)
h.SendDailyReport(ctx, u.ID, u.ChatID)
store.MarkReportSent(u.ID, today)
}
}
// parseAllowedUsers parses a comma-separated list of Telegram user IDs into a set.
func parseAllowedUsers(raw string) map[int64]bool {
m := make(map[int64]bool)
for _, s := range strings.Split(raw, ",") {
s = strings.TrimSpace(s)
if s == "" {
continue
}
id, err := strconv.ParseInt(s, 10, 64)
if err == nil {
m[id] = true
} else {
slog.Warn("invalid user ID in BOT_ALLOWED_USERS", "value", s)
}
}
return m
}
// getEnvDefault returns the environment variable value or a default if unset or empty.
func getEnvDefault(key, defaultValue string) string {
if val := os.Getenv(key); val != "" {
return val
}
return defaultValue
}