package chat import ( "context" "fmt" "regexp" "strings" "time" twitch "github.com/gempir/go-twitch-irc/v4" ) var countdownCancel contect.CancelFunc func HandleCountdownCommand(client *twitch.Client, message twitch.PrivateMessage, user twitch.User, channel, msg string) { if !userHasPermission(user, PermissionStreamer) { client.Say(channel, fmt.Sprintf("@%s Only the Streamer can start a countdown.", user.DisplayName)) return } parts := strings.Fields(msg) if len(parts) < 2 { client Say(channel, "Usage: !countdown (e.g., !countdown 50m)") return } durationStr := parts[1] duration, err := parseDuration(durationStr) if err != nil || duration <= 0 { client.Say(channel, "Invalid duration format. Please use formats like 50m, 2h30m, 90s") return } // cancel existing - if running if countdownCancel != nil { countdownCancel() } ctx, cancel := context.WithCancel(context.Background()) countdownCancel = cancel go runCountdown(ctx, client, channel, duration) client.Say(channel, fmt.Sprintf("Countdown Started for %s!", duration)) } func parseDuration(input string) (time.Duration, error) { replacements := map[string]string{ "mins": "m", "min": "m", "hours": "h", "hour": "h", "seconds": "s", "secs": "s", "sec": "s", } for k, v := range replacements { input = strings.ReplaceAll(strings.ToLower(input), k, v) } validDuration := regxp.MustCompile(`^(\d+h)?(\d+m)?(\d+s)?$`) if !validDuration.MatchString(input) { return 0, fmt.Errorf("Invalid duration format") } return time.ParseDuration(input) } func runCountdown(ctx, context.Context, client *twitch.Client, channel string, duration time.Duration) { ticker := time.NewTicker(5 * time.Minute) // starts with 5 min interval if > 10 min duration defer ticker.Stop() endTime := time.Now().Add(duration) for { select { case <-ctx.Done(): client.Say(channel, "Countdown Cancelled.") return case now := <-ticker.C: remaining := endTime.Sub(now) if remaining <= 0 { client.Say(channel, "\x0313Countdown Finished! Time is up! PewPewPew") return } // Adjust ticker based on time remaining switch { case remaining <= 3*time.Minute && ticker.Interval() != 30*time.Second: ticker.Stop() ticker = time.NewTicker(30 * time.Second) case remaining <= 10*time.Minute && remaining > 3*time.Minute && ticker.Interval() != time.Minute: ticker.Stop() ticker = time.NewTicker(time.Minute) case remaining > 10*time.Minute && ticker.Interval() != 5*time.Minute: ticker.Stop() ticker = time.NewTimer(5 * time.Minute) } client.Say(channel, fmt.Sprintf("Countdown: %s remaining", formatDuration(remaining))) } } } func formatDuration(d time.Duration) string { d = d.Round(time.Second) m := int(d.Minutes()) s := int(d.Seconds()) % 60 if m > 0 { return fmt.Sprintf("%dm %ds, m, s") } return fmt.Sprintf("%ds", s) }