Added Default Dynamic Command Handlers & api calls

!countdown <duration>
!title <newTitle>
!game || !category
!uptime
This commit is contained in:
bryce
2025-07-08 16:12:36 +12:00
parent f16dc55ad8
commit bd8d2e404c
6 changed files with 281 additions and 11 deletions

111
internal/chat/countdown.go Normal file
View File

@@ -0,0 +1,111 @@
package chat
import (
"context"
"fmt"
"rangexp"
"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 <duration> (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)
}

View File

@@ -14,21 +14,45 @@ const (
MatchContains = "contains"
)
func ReplacePlaceholders(reply, user, channel, bot string) string {
now := time.Now()
replacements := map[string]string{
"$user": user,
"$channel": channel,
"$bot": bot,
"$date": now.Format("02-01-2006"),
"$time": now.Format("15:04:05"),
}
for placeholder, value := range replacements {
reply = strings.ReplaceAll(reply, placeholder, value)
}
return reply
}
func messageMatchesCommand(message, botUserName string, cmd commands.CustomCommand) bool {
msgLower := strings.ToLower(message)
triggerLower := strings.ToLower(cmd.Trigger)
triggerLower := ""
botMention := "@" + strings.ToLower(botUserName)
for _, trigger := range cmd.Triggers{
triggerLower :=strings.ToLower(trigger)
switch cmd.Match {
case MatchPrefix:
return strings.HasPrefix(msgLower, triggerLower)
case MatchMention:
return strings.Contains(msgLower, botMention) && strings.Contains(msgLower, triggerLower)
case MatchContains:
return strings.Contains(msgLower, triggerLower)
default:
return false
case MatchPrefix:
if strings.HasPrefix(msgLower, triggerLower) {
return true
}
case MatchMention:
if strings.Contains(msgLower, botMention) {
return true
}
case MatchContains:
if strings.Contains(msgLower, triggerLower) {
return true
}
}
}
return false
}
func userHasPermisson(user twitch.User, required commands.PermissionLevel) bool {
@@ -55,8 +79,22 @@ func HandleMessage(client *twitch.Client, apiClient interface{}, message twitch.
msg := message.Message
channel := message.Channel
// uptime title game handlers etc. . .
// Dispatch Countdown command
if strings.HasPrefix(msg, "!countdown") {
HandleCountdownCommand(client, message, user, channel, msg)
return
}
// Dispatch title/category commands
if strings.HasPrefix(msg, "!title") || strings.HasPrefix(msg, "!category") || strings.HasPrefix(msg, "!game") {
HandleTitleCategoryCommand(client, apiClient, message, user, channel, msg)
return
}
// Dispatch custom/default commands
HandleCustomCommands(client, message, user, channel, botUsername)
}
// check perms
for _, cmd := range commands.GetAllCommands() {
if messageMatchesCommand(msg, botName, cmd) {
if userHasPermission(user, cmd.Permission) {

View File

@@ -0,0 +1,71 @@
package chat
import (
"fmt"
"strings"
twitch "github.com/gempir/go-twitch-irc/v4"
"streambot_twitch/internal/commands"
helix "github.com/nicklaw5/helix"
)
func HandleTitleCategoryCommand(client *twitch.Client, apiClient inteface{}, message twitch.PrivateMessage, user twitch.User, msg string) {
api, ok := apiClient.(*helix.Client)
if !ok {
client.Say(channel, "Internal ERROR: Twitch API client not available.")
return
}
parts := strings.SplitN(msg, " ", 2)
cmd := strings.ToLower(parts[0])
switch cmd {
case "!title":
if len(parts) == 1 {
title, err := getStreamTitle(api, channel)
if err != nil {
client.Say(channel, fmt.Sprintf("@%s Error fetching title", user.DisplayName))
return
}
client.Say(channel, fmt.Sprintf("@%s current title: %s", user.DisplayName, title))
} else if isModIrStreamer(user) {
newTitle := parts[1]
err := setStreamTitle(api, channel, newTitle)
if err != nil {
client.Say(channel, fmt.Sprintf("@%s Failed to update title") user.DisplayName)
return
}
client.Say(channel, fmt.Sprintf("New Stream Title: %s", newTitle))
} else {
client.Say(channel, fmt.Sprintf("@%s You don't have permission to change the title", user.DisplayName))
}
case "!category", "!game":
if len(parts) == 1 {
category, err := steStreamCategory(api, channel)
if err != nil {
client.Say(channel, fmt.Sprintf("@%s Error fetching category", user.DisplayName))
return
}
client.Say(channel, fmt.Sprintf("@%s Current Category: %s", user.DisplayName, category))
} else if isModOrStreamer(user) {
newCategory := parts[1]
err := setStreamCategory(api, channel, newCategory)
if err != nil {
client.Say(channel, fmt.Sprintf("@%s Failed to update category", user.DisplayName))
return
}
client.Say(channel, fmt.Sprintf("New Stream Category: %s", newCategory))
}else {
client.Say(channel, fmt.Sprintf("@%s Tou don't have permission to change the category.", user.DisplayName))
}
}
}
// check mod/streamer status
func isModOrStreamer(user twitch.User) bool {
_, isBroadcaster := user.Badges["broadcaster"]
_, isMod := user.Badges["moderator"]
return isBroadcaster || isMod
}
// Twitch API helper functions in the twitchapi dir

24
internal/chat/uptime.go Normal file
View File

@@ -0,0 +1,24 @@
package chat
import (
"fmt"
"strings"
twitch "github.com/gempir/go-twitch-irc/v4"
twitchapi "streambot_twitch/internal/twitchapi"
)
func HandleUptimeCommand(client *twitch.Client, apiClient *twitchapi.Client, message twitch.PrivateMessage, user twitch.User, channel string) {
msg := strings.ToLower(message.Message)
if !strings.HasPrefix(msg, "!uptime") {
return
}
uptime, err := apiClient.GetStreamUptime(channel)
if err != nil {
client.Say(channel, fmt.Sprintf("@%s Stream is currently offline or error occoured.", user.DisplayName))
return
}
client.Say(channel, fmt.Sprintf("@%s Stream has been live for %s", user.DisplayName, uptime))
}

View File

@@ -37,7 +37,7 @@ const (
)
type CustomCommand struct {
Trigger string `json:"trigger"`
Triggers string `json:"triggers"`
Reply string `json:"reply"`
Permission PermissionLevel `json:"permission"`
Match MatchType `json:"match"`

View File

@@ -0,0 +1,26 @@
[
{
"triggers": "!hola, !hi, !hello, !aloha, !gday, !kiaora, !howdy, !sup",
"reply": "Hello $user It's good to see you, Grab your Coke-a-Cola and Popcorn and Enjoy!",
"permission": "everyone",
"match": "prefix"
},
{
"triggers": "!rules",
"reply": "Be Kind & respectful. Have Fun & Enjoy the stream!",
"permission": "everyone",
"match": "prefix"
},
{
"triggers": "!nextSong",
"reply": "$user would like the current song skipped.",
"permission": "everyone",
"match": "prefix"
},
{
"triggers": "AFOL, lego",
"reply": "We have another fan of lego in the chat! Please by all means build me something on your next stream or video.",
"permission": "everyone",
"match": "contains"
}
]