From 8596ce3f6dd3182a6342ce7ba485c92a464b4db5 Mon Sep 17 00:00:00 2001 From: Ash Keel Date: Sat, 18 Sep 2021 22:06:22 +0200 Subject: [PATCH] Add callable functions to custom commands --- frontend/src/ui/pages/twitch/Commands.tsx | 2 +- go.mod | 7 +++ go.sum | 18 +++++++ main.go | 3 ++ modules/loyalty/manager.go | 40 ++++++++------ modules/twitch/bot.go | 63 +++++++++++++++++------ modules/twitch/commands.go | 51 +++++++++++++++++- modules/twitch/module.go | 2 + 8 files changed, 151 insertions(+), 35 deletions(-) diff --git a/frontend/src/ui/pages/twitch/Commands.tsx b/frontend/src/ui/pages/twitch/Commands.tsx index 8b56289..0cbc7d6 100644 --- a/frontend/src/ui/pages/twitch/Commands.tsx +++ b/frontend/src/ui/pages/twitch/Commands.tsx @@ -225,11 +225,11 @@ export default function TwitchBotCommandsPage( dispatch( setCommands({ ...commands, + [oldName]: undefined, [newName]: { ...commands[oldName], ...data, }, - [oldName]: undefined, }), ); setShowModifyCommand(null); diff --git a/go.mod b/go.mod index 9fd5014..0a81cdb 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,17 @@ module github.com/strimertul/strimertul go 1.16 require ( + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/sprig v2.22.0+incompatible // indirect github.com/dgraph-io/badger/v3 v3.2011.1 github.com/gempir/go-twitch-irc/v2 v2.5.0 + github.com/google/uuid v1.3.0 // indirect + github.com/huandu/xstrings v1.3.2 // indirect + github.com/imdario/mergo v0.3.12 // indirect github.com/json-iterator/go v1.1.11 github.com/mattn/go-colorable v0.1.8 + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/nicklaw5/helix v1.15.0 github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 github.com/sirupsen/logrus v1.8.1 diff --git a/go.sum b/go.sum index 313a61e..9bcf504 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,12 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM= github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -44,11 +50,17 @@ github.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-dap v0.2.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -63,8 +75,12 @@ github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mmcloughlin/avo v0.0.0-20201105074841-5d2f697d268f/go.mod h1:6aKT4zZIrpGqB3RpFU14ByCSSyKY6LfJz4J/JJChHfI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -131,6 +147,7 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210505212654-3497b51f5e64 h1:QuAh/1Gwc0d+u9walMU1NqzhRemNegsv5esp2ALQIY4= golang.org/x/crypto v0.0.0-20210505212654-3497b51f5e64/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -190,5 +207,6 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/main.go b/main.go index d0605b3..f6d69a9 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "flag" "fmt" "io/fs" + "math/rand" "net/http" "runtime" "time" @@ -69,6 +70,8 @@ func main() { loglevel := flag.String("loglevel", "info", "Logging level (debug, info, warn, error)") flag.Parse() + rand.Seed(time.Now().UnixNano()) + log.SetLevel(parseLogLevel(*loglevel)) // Ok this is dumb but listen, I like colors. diff --git a/modules/loyalty/manager.go b/modules/loyalty/manager.go index 8ae3c83..b7cb7ae 100644 --- a/modules/loyalty/manager.go +++ b/modules/loyalty/manager.go @@ -110,21 +110,29 @@ func (m *Manager) update(kvs []database.ModifiedKV) error { // Check for config changes/RPC switch key { case ConfigKey: - m.mu.Lock() - err = jsoniter.ConfigFastest.Unmarshal(kv.Data, &m.config) - m.mu.Unlock() + err = func() error { + m.mu.Lock() + defer m.mu.Unlock() + return jsoniter.ConfigFastest.Unmarshal(kv.Data, &m.config) + }() case GoalsKey: - m.mu.Lock() - err = jsoniter.ConfigFastest.Unmarshal(kv.Data, &m.goals) - m.mu.Unlock() + err = func() error { + m.mu.Lock() + defer m.mu.Unlock() + return jsoniter.ConfigFastest.Unmarshal(kv.Data, &m.goals) + }() case RewardsKey: - m.mu.Lock() - err = jsoniter.ConfigFastest.Unmarshal(kv.Data, &m.rewards) - m.mu.Unlock() + err = func() error { + m.mu.Lock() + defer m.mu.Unlock() + return jsoniter.ConfigFastest.Unmarshal(kv.Data, &m.rewards) + }() case QueueKey: - m.mu.Lock() - err = jsoniter.ConfigFastest.Unmarshal(kv.Data, &m.queue) - m.mu.Unlock() + err = func() error { + m.mu.Lock() + defer m.mu.Unlock() + return jsoniter.ConfigFastest.Unmarshal(kv.Data, &m.queue) + }() case CreateRedeemRPC: var redeem Redeem err = jsoniter.ConfigFastest.Unmarshal(kv.Data, &redeem) @@ -145,9 +153,11 @@ func (m *Manager) update(kvs []database.ModifiedKV) error { var entry PointsEntry err = jsoniter.ConfigFastest.Unmarshal(kv.Data, &entry) user := kv.Key[len(PointsPrefix):] - m.mu.Lock() - m.points[user] = entry - m.mu.Unlock() + func() { + m.mu.Lock() + defer m.mu.Unlock() + m.points[user] = entry + }() } } if err != nil { diff --git a/modules/twitch/bot.go b/modules/twitch/bot.go index ac8c488..45ec502 100644 --- a/modules/twitch/bot.go +++ b/modules/twitch/bot.go @@ -4,8 +4,10 @@ import ( "context" "strings" "sync" + "text/template" "time" + "github.com/Masterminds/sprig" irc "github.com/gempir/go-twitch-irc/v2" jsoniter "github.com/json-iterator/go" "github.com/sirupsen/logrus" @@ -25,8 +27,10 @@ type Bot struct { banlist map[string]bool chatHistory []irc.PrivateMessage - commands map[string]BotCommand - customCommands map[string]BotCustomCommand + commands map[string]BotCommand + customCommands map[string]BotCustomCommand + customTemplates map[string]*template.Template + customFunctions template.FuncMap mu sync.Mutex @@ -39,17 +43,18 @@ func NewBot(api *Client, config BotConfig) *Bot { client := irc.NewClient(config.Username, config.Token) bot := &Bot{ - Client: client, - username: strings.ToLower(config.Username), // Normalize username - config: config, - logger: api.logger, - api: api, - lastMessage: time.Now(), - activeUsers: make(map[string]bool), - banlist: make(map[string]bool), - mu: sync.Mutex{}, - commands: make(map[string]BotCommand), - customCommands: make(map[string]BotCustomCommand), + Client: client, + username: strings.ToLower(config.Username), // Normalize username + config: config, + logger: api.logger, + api: api, + lastMessage: time.Now(), + activeUsers: make(map[string]bool), + banlist: make(map[string]bool), + mu: sync.Mutex{}, + commands: make(map[string]BotCommand), + customCommands: make(map[string]BotCustomCommand), + customTemplates: make(map[string]*template.Template), } client.OnPrivateMessage(func(message irc.PrivateMessage) { @@ -81,7 +86,7 @@ func NewBot(api *Client, config BotConfig) *Bot { continue } if strings.HasPrefix(message.Message, cmd) { - go cmdCustom(bot, data, message) + go cmdCustom(bot, cmd, data, message) bot.lastMessage = time.Now() } } @@ -124,7 +129,12 @@ func NewBot(api *Client, config BotConfig) *Bot { bot.Client.Join(config.Channel) // Load custom commands + bot.setupFunctions() api.db.GetJSON(CustomCommandsKey, &bot.customCommands) + err := bot.updateTemplates() + if err != nil { + bot.logger.WithError(err).Error("failed to load custom commands") + } go api.db.Subscribe(context.Background(), bot.updateCommands, CustomCommandsKey) return bot @@ -135,9 +145,28 @@ func (b *Bot) updateCommands(kvs []database.ModifiedKV) error { key := string(kv.Key) switch key { case CustomCommandsKey: - b.mu.Lock() - err := jsoniter.ConfigFastest.Unmarshal(kv.Data, &b.customCommands) - b.mu.Unlock() + err := func() error { + b.mu.Lock() + defer b.mu.Unlock() + return jsoniter.ConfigFastest.Unmarshal(kv.Data, &b.customCommands) + }() + if err != nil { + return err + } + // Recreate templates + if err := b.updateTemplates(); err != nil { + return err + } + } + } + return nil +} + +func (b *Bot) updateTemplates() error { + for cmd, tmpl := range b.customCommands { + var err error + b.customTemplates[cmd], err = template.New("").Funcs(sprig.TxtFuncMap()).Funcs(b.customFunctions).Parse(tmpl.Response) + if err != nil { return err } } diff --git a/modules/twitch/commands.go b/modules/twitch/commands.go index 166d083..4d93087 100644 --- a/modules/twitch/commands.go +++ b/modules/twitch/commands.go @@ -1,7 +1,14 @@ package twitch import ( + "bytes" + "math/rand" + "strconv" + "strings" + "text/template" + irc "github.com/gempir/go-twitch-irc/v2" + "github.com/nicklaw5/helix" ) type AccessLevelType string @@ -23,7 +30,47 @@ type BotCommand struct { Enabled bool } -func cmdCustom(bot *Bot, cmd BotCustomCommand, message irc.PrivateMessage) { +func cmdCustom(bot *Bot, cmd string, data BotCustomCommand, message irc.PrivateMessage) { // Add future logic (like counters etc) here, for now it's just fixed messages - bot.Client.Say(message.Channel, cmd.Response) + var buf bytes.Buffer + err := bot.customTemplates[cmd].Execute(&buf, message) + if err != nil { + bot.logger.WithError(err).Error("Failed to execute custom command template") + return + } + bot.Client.Say(message.Channel, buf.String()) +} + +func (b *Bot) setupFunctions() { + b.customFunctions = template.FuncMap{ + "user": func(message irc.PrivateMessage) string { + return message.User.DisplayName + }, + "param": func(num int, message irc.PrivateMessage) string { + parts := strings.Split(message.Message, " ") + if num >= len(parts) { + return parts[len(parts)-1] + } + return parts[num] + }, + "randomInt": func(min int, max int) int { + return rand.Intn(max-min) + min + }, + "game": func(channel string) string { + info, err := b.api.API.SearchChannels(&helix.SearchChannelsParams{Channel: channel, First: 1, LiveOnly: false}) + if err != nil { + return "unknown" + } + return info.Data.Channels[0].GameName + }, + "count": func(name string) int { + counter := 0 + if byt, err := b.api.db.GetKey(BotCounterPrefix + name); err == nil { + counter, _ = strconv.Atoi(string(byt)) + } + counter += 1 + b.api.db.PutKey(BotCounterPrefix+name, []byte(strconv.Itoa(counter))) + return counter + }, + } } diff --git a/modules/twitch/module.go b/modules/twitch/module.go index 996497e..c42b3e7 100644 --- a/modules/twitch/module.go +++ b/modules/twitch/module.go @@ -31,3 +31,5 @@ type BotCustomCommand struct { const CustomCommandsKey = "twitch/bot-custom-commands" const WriteMessageRPC = "twitch/@send-chat-message" + +const BotCounterPrefix = "twitch/bot-counters/"