Started REPL env for custom interactive shell
ONLY active when bot is running - Arguments [--watch] displays how many usses of each trigger and how many uses over the lifetime ofthe bot Commands - title [get/set] <newTitle> - category [get/set] <newCategory> - MUST be a legit Twitch.tv one - yet to come [cmd add|del|edit] [trigger] [response] (NOTE if you want Emotes you will have to type the 'name' in for twitch chat to recognise it)
This commit is contained in:
96
cmd/cli/main.go
Normal file
96
cmd/cli/main.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
twitchapi "streambot_twitch/internal/twitchapi"
|
||||||
|
"streambot_twitch/internal/commands"
|
||||||
|
"streambot_twitch/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// env setup
|
||||||
|
clientID := os.Getenv("TWITCH_CLIENT_ID")
|
||||||
|
clientSecret := os.Getenv("TWITCH_CLIENT_SECRET")
|
||||||
|
streamerChan := os.Getenv("CHANNEL")
|
||||||
|
userToken := os.Getenv("STREAMER_OAUTH")
|
||||||
|
|
||||||
|
if clientID == "" || clientSecret == "" || streamerChan == "" {
|
||||||
|
log.Fatal("Set TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET or cHANNEL")
|
||||||
|
}
|
||||||
|
|
||||||
|
// init config, commands, usage
|
||||||
|
if err := config.Load(); err != nil {log.Fatal(err)}
|
||||||
|
if err := commands.LoadDefaultCommands(); err != nil {log.Fatal(err)}
|
||||||
|
if err := commands.LoadCustomCommands(); err != nil {lof.Fatal(err)}
|
||||||
|
if err := commands.LoadUsage(); err != nil {log.Fatal(err)}
|
||||||
|
|
||||||
|
// init api client
|
||||||
|
apiClient, err := twitchapi.NewClient(clientID, clientSecret)
|
||||||
|
if err != nil {log.Fatalf("API client: %v", err)}
|
||||||
|
if userToken != "" {
|
||||||
|
apiClient.SetUserAccessToken(userToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI flags
|
||||||
|
watch := flag.Bool("watch", false, "refresh stats periodically")
|
||||||
|
interval := flag.Duration("interval", 5*time.Minute, "refresh interval for watch mode")
|
||||||
|
flag.Parse()
|
||||||
|
args := flag.Args()
|
||||||
|
|
||||||
|
if len(args) == 0 || strings.ToLower(args[0]) == "shell" {
|
||||||
|
repl(apiClient, streamerChan, *watch, *interval)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(apiClient, streamerChan, args, *watch, *interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
// repl interactive prompt
|
||||||
|
func repl(apiClient *twitchapi.Client, streamerChan string, watch bool, interval time.Duration) {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
for {
|
||||||
|
fmt.Print("bot:> ")
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if line == "exit" || line == "quit" {
|
||||||
|
fmt.Println("bye")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
dispatch(apiClient, streamerChan, parts, watch, interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatch handles one command invocation
|
||||||
|
func dispatch(apiClient *twitchapi.Client, streamerChan string, args []string, watch bool, interval time.Duration) {
|
||||||
|
cmd := strings.ToLower(args[0])
|
||||||
|
switch cmd {
|
||||||
|
case "stats":
|
||||||
|
if watch {
|
||||||
|
printUsage()
|
||||||
|
t := time.NewTicker(interval)
|
||||||
|
for range t.C {
|
||||||
|
printUsage()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printUsage()
|
||||||
|
}
|
||||||
|
|
||||||
|
case "title":
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -97,7 +97,13 @@ func HandleMessage(client *twitch.Client, apiClient interface{}, message twitch.
|
|||||||
// check perms
|
// check perms
|
||||||
for _, cmd := range commands.GetAllCommands() {
|
for _, cmd := range commands.GetAllCommands() {
|
||||||
if messageMatchesCommand(msg, botName, cmd) {
|
if messageMatchesCommand(msg, botName, cmd) {
|
||||||
|
// track usage: use first alias as key
|
||||||
|
triggerKey := strings.ToLower(cmd.Triggers[0])
|
||||||
|
commands.IncrementUsage(triggerKey)
|
||||||
|
|
||||||
if userHasPermission(user, cmd.Permission) {
|
if userHasPermission(user, cmd.Permission) {
|
||||||
|
reply := ReplacePlaceholders(cmd.Replay, user.DisplayName, channel, botUsername)
|
||||||
|
|
||||||
client.Say(channel, cmd.Reply)
|
client.Say(channel, cmd.Reply)
|
||||||
} else {
|
} else {
|
||||||
client.Say(channel, fmt.Sprintf("@%s You don't have permission to use this command.", user.DisplayName))
|
client.Say(channel, fmt.Sprintf("@%s You don't have permission to use this command.", user.DisplayName))
|
||||||
|
75
internal/commands/usage.go
Normal file
75
internal/commands/usage.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const usageFile = "internal/storage/usage.json"
|
||||||
|
|
||||||
|
var (
|
||||||
|
usageMu sybc.Mutex
|
||||||
|
usageCounts map[string]int
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadUsage() error {
|
||||||
|
usageMu.Lock()
|
||||||
|
defer usageMu.Unlock()
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile(usageFile)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
usageCounts = make(map[string]int)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(dat, &usageCounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveUsage() error {
|
||||||
|
usageMu.Lock()
|
||||||
|
defer usageMu.Unlock()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(usageFile), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data, err := json.MarshalIndent(usageCounts, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(usageFile, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IncrementUsage(trigger string) {
|
||||||
|
usageMu.Lock()
|
||||||
|
defer usageMu.Unlock()
|
||||||
|
if usageCounts == nil {
|
||||||
|
usageCounts = make(map[string]int)
|
||||||
|
}
|
||||||
|
usageCounts[trigger]++
|
||||||
|
SaveUsage()
|
||||||
|
}
|
||||||
|
|
||||||
|
func AllUsage() map[string]int {
|
||||||
|
usageMu.Lock()
|
||||||
|
defer usageMu.Unlock()
|
||||||
|
m := make(map[string]int, len(usageCounts))
|
||||||
|
for k, v := range usageCounts {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func TotalUsage() int {
|
||||||
|
usageMu.Lock()
|
||||||
|
defer usageMu.Unlock()
|
||||||
|
sum := 0
|
||||||
|
for _, v := range usageCounts {
|
||||||
|
sum += v
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
84
internal/config/config.go
Normal file
84
internal/config/config.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConfigDir = "internal/storage"
|
||||||
|
ConfigFile = "settings.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
TitleGetTemplate string `json:"title_get_template"`
|
||||||
|
TitleSetTemplate string `json:"title_set_template"`
|
||||||
|
CategoryGetTemplate string `json:"category_get_template"`
|
||||||
|
CategorySetTemplate string `json:"category_set_template"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Settings
|
||||||
|
|
||||||
|
func Load() error {
|
||||||
|
path := filepath.Join(ConfigDir, ConfigFile)
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
// defaults
|
||||||
|
cfg = Settings{
|
||||||
|
TitleGetTemplate: "@user Current Title: $title",
|
||||||
|
TitleSetTemplate: "New Stream Title: $title",
|
||||||
|
CategoryGetTepmplate: "@user Current Category: $category",
|
||||||
|
CategorySetTemplate: "New StreamCategory: $category",
|
||||||
|
}
|
||||||
|
return Save()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, &cgf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Save() error {
|
||||||
|
if err := os.MkdirAll(ConfigDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path := filepath.Join(ConfigDir, ConfigFile)
|
||||||
|
data, err := json.MarshalIndent(cfg, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(path, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get(key string) (string bool) {
|
||||||
|
switch key {
|
||||||
|
case "title-get":
|
||||||
|
return cfg.TitleGetTemplate, true
|
||||||
|
case "title-set":
|
||||||
|
return cfg.TitleSetTemplate, true
|
||||||
|
case "category-get":
|
||||||
|
return cfg.CategoryGetTemplate, true
|
||||||
|
caes "categroy-set":
|
||||||
|
return cfg.CategorySetTemplate, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func Set(key, val string) bool {
|
||||||
|
switch key {
|
||||||
|
case "title-get":
|
||||||
|
cfg.TitleGetTemplate = val
|
||||||
|
case "tiel-set":
|
||||||
|
cfg.TitleSetTemplate = val
|
||||||
|
case "category-get":
|
||||||
|
cfg.CategoryGetTemplate = val
|
||||||
|
case "category-set":
|
||||||
|
cfg.CategorySetTemplate = val
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
Save()
|
||||||
|
return true
|
||||||
|
}
|
0
internal/storage/settings.json
Normal file
0
internal/storage/settings.json
Normal file
0
internal/storage/usage.json
Normal file
0
internal/storage/usage.json
Normal file
@@ -118,25 +118,25 @@ func (c *Client) GetStreamCategory(channel string) (string, error) {
|
|||||||
return resp.Data.Channels[0].GameName, nil
|
return resp.Data.Channels[0].GameName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setStreamCategory(client *helix.Client, channel, newCategory string) error {
|
func (c *Client) SetStreamCategory(channel, newCategory string) error {
|
||||||
userID, err := GetUserID(client, channel)
|
userID, err := c.GetUserID(channel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// get Twitch GameID by Name from API
|
// get Twitch GameID by Name from API
|
||||||
gameID, err := getGameIDByName(client, newCategory)
|
gameID, err := c.GetGameIDByName(newCategory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = client.EditChannelInformation(&helix.EditChannelInformationParams{
|
_, err = c.api.EditChannelInformation(&helix.EditChannelInformationParams{
|
||||||
BroadcasterID: userID,
|
BroadcasterID: userID,
|
||||||
GameID: gameID,
|
GameID: gameID,
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getGameIDByName(client *helix.Client, name string) (string, error) {
|
func (c *Client) GetGameIDByName(name string) (string, error) {
|
||||||
resp, err := GetGames(&helix.GamesParams{
|
resp, err := c.api.GetGames(&helix.GamesParams{
|
||||||
Names: []string{name},
|
Names: []string{name},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Reference in New Issue
Block a user