package bot import ( "fmt" "log" "os" "strconv" "strings" "time" "github.com/robfig/cron/v3" "github.com/slack-go/slack" "github.com/slack-go/slack/slackevents" "github.com/slack-go/slack/socketmode" "wordle-bot/internal/config" "wordle-bot/internal/tracker" "wordle-bot/pkg/wordlepattern" ) // Bot represents the Slack bot type Bot struct { client *slack.Client tracker *tracker.Tracker config *config.Manager cron *cron.Cron } // New creates a new bot instance func New(botToken, appToken string) (*Bot, error) { t, err := tracker.New("") if err != nil { return nil, fmt.Errorf("failed to create tracker: %w", err) } cfg, err := config.New("") if err != nil { return nil, fmt.Errorf("failed to create config manager: %w", err) } client := slack.New( botToken, slack.OptionAppLevelToken(appToken), ) bot := &Bot{ client: client, tracker: t, config: cfg, cron: cron.New(), } return bot, nil } // Run starts the bot func (b *Bot) Run() error { // Setup cron for daily summaries if err := b.setupCron(); err != nil { return fmt.Errorf("failed to setup cron: %w", err) } b.cron.Start() defer b.cron.Stop() // Create socket mode client client := socketmode.New( b.client, socketmode.OptionLog(log.New(os.Stdout, "socketmode: ", log.Lshortfile|log.LstdFlags)), ) // Handle events go b.handleEvents(client) log.Println("⚡️ Wordle Bot is running!") return client.Run() } func (b *Bot) handleEvents(client *socketmode.Client) { for evt := range client.Events { switch evt.Type { case socketmode.EventTypeEventsAPI: b.handleEventsAPI(client, evt) case socketmode.EventTypeSlashCommand: b.handleSlashCommand(client, evt) } } } func (b *Bot) handleEventsAPI(client *socketmode.Client, evt socketmode.Event) { eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent) if !ok { log.Printf("Ignored %+v\n", evt) return } client.Ack(*evt.Request) if eventsAPIEvent.Type == slackevents.CallbackEvent { innerEvent := eventsAPIEvent.InnerEvent if msgEvent, ok := innerEvent.Data.(*slackevents.MessageEvent); ok { b.handleMessage(msgEvent) } } } func (b *Bot) handleMessage(ev *slackevents.MessageEvent) { // Skip bot messages if ev.BotID != "" { return } // Look for Wordle score match, found := wordlepattern.Find(ev.Text) if !found { return } // Get user info user, err := b.client.GetUserInfo(ev.User) if err != nil { log.Printf("Error getting user info: %v", err) return } userName := user.RealName if userName == "" { userName = user.Name } // Convert timestamp tsFloat, _ := strconv.ParseFloat(ev.TimeStamp, 64) timestamp := time.Unix(int64(tsFloat), 0) // Track the score if err := b.tracker.Add(ev.User, userName, match.PuzzleNumber, match.Score, timestamp); err != nil { log.Printf("Error tracking score: %v", err) return } log.Printf("Tracked score for %s: Wordle %d %s/6", userName, match.PuzzleNumber, match.Score) // React to the message if err := b.client.AddReaction("white_check_mark", slack.ItemRef{ Channel: ev.Channel, Timestamp: ev.TimeStamp, }); err != nil { log.Printf("Error adding reaction: %v", err) } } func (b *Bot) handleSlashCommand(client *socketmode.Client, evt socketmode.Event) { cmd, ok := evt.Data.(slack.SlashCommand) if !ok { log.Printf("Ignored %+v\n", evt) return } var response string var err error switch cmd.Command { case "/wordle-config": response, err = b.handleConfigCommand(cmd) case "/wordle-stats": response, err = b.handleStatsCommand(cmd) default: response = fmt.Sprintf("Unknown command: %s", cmd.Command) } if err != nil { log.Printf("Error handling command: %v", err) response = fmt.Sprintf("Error: %v", err) } client.Ack(*evt.Request, map[string]interface{}{ "text": response, }) } func (b *Bot) handleConfigCommand(cmd slack.SlashCommand) (string, error) { args := strings.Fields(cmd.Text) if len(args) == 0 || args[0] == "help" { return `*Wordle Bot Configuration Commands* ` + "`/wordle-config time HH:MM`" + ` - Set daily summary time (24-hour format) ` + "`/wordle-config channel`" + ` - Set this channel for daily summaries ` + "`/wordle-config status`" + ` - Show current configuration`, nil } switch args[0] { case "time": if len(args) < 2 { return "Please specify time in HH:MM format (e.g., 09:00)", nil } if err := b.config.SetSummaryTime(args[1]); err != nil { return "❌ Invalid time format. Use HH:MM (e.g., 09:00)", nil } // Reschedule cron if err := b.setupCron(); err != nil { return fmt.Sprintf("❌ Error rescheduling: %v", err), nil } return fmt.Sprintf("✅ Daily summary time set to %s", args[1]), nil case "channel": if err := b.config.SetSummaryChannel(cmd.ChannelID); err != nil { return fmt.Sprintf("❌ Error setting channel: %v", err), nil } cfg := b.config.Get() return fmt.Sprintf("✅ Daily summaries will be posted to this channel at %s", cfg.SummaryTime), nil case "status": cfg := b.config.Get() channelText := "Not set" if cfg.SummaryChannel != "" { channelText = fmt.Sprintf("<#%s>", cfg.SummaryChannel) } return fmt.Sprintf(`*Current Configuration* Summary Time: %s Summary Channel: %s`, cfg.SummaryTime, channelText), nil default: return fmt.Sprintf("Unknown command: %s. Use `/wordle-config help` for usage.", args[0]), nil } } func (b *Bot) handleStatsCommand(cmd slack.SlashCommand) (string, error) { args := strings.TrimSpace(cmd.Text) switch args { case "", "yesterday": scores := b.tracker.GetYesterday() return tracker.FormatSummary(scores), nil case "today": scores := b.tracker.GetToday() return tracker.FormatToday(scores), nil default: return "Usage: `/wordle-stats` or `/wordle-stats yesterday` or `/wordle-stats today`", nil } } func (b *Bot) setupCron() error { // Remove all existing jobs for _, entry := range b.cron.Entries() { b.cron.Remove(entry.ID) } cfg := b.config.Get() // Parse time parts := strings.Split(cfg.SummaryTime, ":") if len(parts) != 2 { return fmt.Errorf("invalid time format: %s", cfg.SummaryTime) } hour := parts[0] minute := parts[1] // Create cron schedule (minute hour * * *) schedule := fmt.Sprintf("%s %s * * *", minute, hour) _, err := b.cron.AddFunc(schedule, func() { b.postDailySummary() }) if err != nil { return fmt.Errorf("failed to add cron job: %w", err) } log.Printf("Scheduled daily summary for %s", cfg.SummaryTime) return nil } func (b *Bot) postDailySummary() { cfg := b.config.Get() if cfg.SummaryChannel == "" { log.Println("No summary channel configured, skipping daily summary") return } yesterdayScores := b.tracker.GetYesterday() summary := tracker.FormatSummary(yesterdayScores) _, _, err := b.client.PostMessage( cfg.SummaryChannel, slack.MsgOptionText(summary, false), slack.MsgOptionDisableLinkUnfurl(), ) if err != nil { log.Printf("Error posting summary: %v", err) return } log.Printf("Posted daily summary to %s", cfg.SummaryChannel) }