305 lines
6.9 KiB
Go
305 lines
6.9 KiB
Go
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)
|
|
}
|