Added Default Dynamic Command Handlers & api calls
!countdown <duration> !title <newTitle> !game || !category !uptime
This commit is contained in:
111
internal/chat/countdown.go
Normal file
111
internal/chat/countdown.go
Normal 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)
|
||||||
|
}
|
@@ -14,21 +14,45 @@ const (
|
|||||||
MatchContains = "contains"
|
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 {
|
func messageMatchesCommand(message, botUserName string, cmd commands.CustomCommand) bool {
|
||||||
msgLower := strings.ToLower(message)
|
msgLower := strings.ToLower(message)
|
||||||
triggerLower := strings.ToLower(cmd.Trigger)
|
triggerLower := ""
|
||||||
botMention := "@" + strings.ToLower(botUserName)
|
botMention := "@" + strings.ToLower(botUserName)
|
||||||
|
|
||||||
|
for _, trigger := range cmd.Triggers{
|
||||||
|
triggerLower :=strings.ToLower(trigger)
|
||||||
switch cmd.Match {
|
switch cmd.Match {
|
||||||
case MatchPrefix:
|
case MatchPrefix:
|
||||||
return strings.HasPrefix(msgLower, triggerLower)
|
if strings.HasPrefix(msgLower, triggerLower) {
|
||||||
case MatchMention:
|
return true
|
||||||
return strings.Contains(msgLower, botMention) && strings.Contains(msgLower, triggerLower)
|
}
|
||||||
case MatchContains:
|
case MatchMention:
|
||||||
return strings.Contains(msgLower, triggerLower)
|
if strings.Contains(msgLower, botMention) {
|
||||||
default:
|
return true
|
||||||
return false
|
}
|
||||||
|
case MatchContains:
|
||||||
|
if strings.Contains(msgLower, triggerLower) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func userHasPermisson(user twitch.User, required commands.PermissionLevel) bool {
|
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
|
msg := message.Message
|
||||||
channel := message.Channel
|
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() {
|
for _, cmd := range commands.GetAllCommands() {
|
||||||
if messageMatchesCommand(msg, botName, cmd) {
|
if messageMatchesCommand(msg, botName, cmd) {
|
||||||
if userHasPermission(user, cmd.Permission) {
|
if userHasPermission(user, cmd.Permission) {
|
||||||
|
71
internal/chat/title_category.go
Normal file
71
internal/chat/title_category.go
Normal 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
24
internal/chat/uptime.go
Normal 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))
|
||||||
|
}
|
@@ -37,7 +37,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CustomCommand struct {
|
type CustomCommand struct {
|
||||||
Trigger string `json:"trigger"`
|
Triggers string `json:"triggers"`
|
||||||
Reply string `json:"reply"`
|
Reply string `json:"reply"`
|
||||||
Permission PermissionLevel `json:"permission"`
|
Permission PermissionLevel `json:"permission"`
|
||||||
Match MatchType `json:"match"`
|
Match MatchType `json:"match"`
|
||||||
|
26
internal/storage/default_commands.json
Normal file
26
internal/storage/default_commands.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
Reference in New Issue
Block a user