mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-12-30 00:21:16 +00:00
Add command action to execute arbitrary logic
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
b24ca61b8e
commit
6d9484be2d
4 changed files with 132 additions and 12 deletions
71
action_script.go
Normal file
71
action_script.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/go-irc/irc"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
|
||||
if len(r.Command) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.CommandTimeout)
|
||||
defer cancel()
|
||||
|
||||
var (
|
||||
stdin = new(bytes.Buffer)
|
||||
stdout = new(bytes.Buffer)
|
||||
)
|
||||
|
||||
if err := json.NewEncoder(stdin).Encode(map[string]interface{}{
|
||||
"badges": ircHandler{}.ParseBadgeLevels(m),
|
||||
"channel": m.Params[0],
|
||||
"message": m.Trailing(),
|
||||
"tags": m.Tags,
|
||||
"username": m.User,
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "encoding script input")
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, r.Command[0], r.Command[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = stdin
|
||||
cmd.Stdout = stdout
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return errors.Wrap(err, "running command")
|
||||
}
|
||||
|
||||
if stdout.Len() == 0 {
|
||||
// Script was successful but did not yield actions
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
actions []*ruleAction
|
||||
decoder = json.NewDecoder(stdout)
|
||||
)
|
||||
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&actions); err != nil {
|
||||
return errors.Wrap(err, "decoding actions output")
|
||||
}
|
||||
|
||||
for _, action := range actions {
|
||||
if err := triggerActions(c, m, ruleDef, action); err != nil {
|
||||
return errors.Wrap(err, "execute returned action")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
13
config.go
13
config.go
|
@ -177,12 +177,13 @@ func (r *rule) Matches(m *irc.Message, event *string) bool {
|
|||
}
|
||||
|
||||
type ruleAction struct {
|
||||
Ban *string `yaml:"ban"`
|
||||
CounterStep *int64 `yaml:"counter_step"`
|
||||
Counter *string `yaml:"counter"`
|
||||
DeleteMessage *bool `yaml:"delete_message"`
|
||||
Respond *string `yaml:"respond"`
|
||||
Timeout *time.Duration `yaml:"timeout"`
|
||||
Ban *string `json:"ban" yaml:"ban"`
|
||||
Command []string `json:"command" yaml:"command"`
|
||||
CounterStep *int64 `json:"counter_step" yaml:"counter_step"`
|
||||
Counter *string `json:"counter" yaml:"counter"`
|
||||
DeleteMessage *bool `json:"delete_message" yaml:"delete_message"`
|
||||
Respond *string `json:"respond" yaml:"respond"`
|
||||
Timeout *time.Duration `json:"timeout" yaml:"timeout"`
|
||||
}
|
||||
|
||||
func loadConfig(filename string) error {
|
||||
|
|
13
main.go
13
main.go
|
@ -14,12 +14,13 @@ import (
|
|||
|
||||
var (
|
||||
cfg = struct {
|
||||
Config string `flag:"config,c" default:"./config.yaml" description:"Location of configuration file"`
|
||||
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
||||
StorageFile string `flag:"storage-file" default:"./storage.json.gz" description:"Where to store the data"`
|
||||
TwitchClient string `flag:"twitch-client" default:"" description:"Client ID to act as" validate:"nonzero"`
|
||||
TwitchToken string `flag:"twitch-token" default:"" description:"OAuth token valid for client"`
|
||||
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||||
CommandTimeout time.Duration `flag:"command-timeout" default:"30s" description:"Timeout for command execution"`
|
||||
Config string `flag:"config,c" default:"./config.yaml" description:"Location of configuration file"`
|
||||
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
||||
StorageFile string `flag:"storage-file" default:"./storage.json.gz" description:"Where to store the data"`
|
||||
TwitchClient string `flag:"twitch-client" default:"" description:"Client ID to act as" validate:"nonzero"`
|
||||
TwitchToken string `flag:"twitch-token" default:"" description:"OAuth token valid for client"`
|
||||
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||||
}{}
|
||||
|
||||
config *configFile
|
||||
|
|
47
wiki/Home.md
47
wiki/Home.md
|
@ -19,6 +19,9 @@ rules: # See below for examples
|
|||
# Issue a ban on the user who wrote the chat-line
|
||||
- ban: "reason of ban"
|
||||
|
||||
# Command to execute for the chat message, must return an JSON encoded array of actions
|
||||
- command: [/bin/bash, -c, "echo '[{\"respond\": \"Text\"}]'"]
|
||||
|
||||
# Modify an internal counter value (does NOT send a chat line)
|
||||
- counter: "counterid" # String to identify the counter, applies templating
|
||||
counter_step: 1 # Integer, can be negative or positive, default: +1
|
||||
|
@ -81,6 +84,50 @@ Additionally there are some functions available in the templates:
|
|||
- `recentGame <username> [fallback]` - Returns the last played game name of the specified user (see shoutout example) or the `fallback` if the game could not be fetched. If no fallback was supplied the message will fail and not be sent.
|
||||
- `tag <tagname>` - Takes the message sent to the channel, returns the value of the tag specified
|
||||
|
||||
## Command executions
|
||||
|
||||
Your command will get a JSON object passed through `stdin` you can parse to gain details about the message. It is expected to yield an array of actions on `stdout` and exit with status `0`. If it does not the action will be marked failed. In case you need to output debug output you can use `stderr` which is directly piped to the bots `stderr`.
|
||||
|
||||
This is an example input you might get on `stdin`:
|
||||
|
||||
```json
|
||||
{
|
||||
"badges": {
|
||||
"glhf-pledge": 1,
|
||||
"moderator": 1
|
||||
},
|
||||
"channel": "#tezrian",
|
||||
"message": "!test",
|
||||
"tags": {
|
||||
"badge-info": "",
|
||||
"badges": "moderator/1,glhf-pledge/1",
|
||||
"client-nonce": "6801c82a341f728dbbaad87ef30eae49",
|
||||
"color": "#A72920",
|
||||
"display-name": "Luziferus",
|
||||
"emotes": "",
|
||||
"flags": "",
|
||||
"id": "dca06466-3741-4b22-8339-4cb5b07a02cc",
|
||||
"mod": "1",
|
||||
"room-id": "485884564",
|
||||
"subscriber": "0",
|
||||
"tmi-sent-ts": "1610313040489",
|
||||
"turbo": "0",
|
||||
"user-id": "69699328",
|
||||
"user-type": "mod"
|
||||
},
|
||||
"username": "luziferus"
|
||||
}
|
||||
```
|
||||
|
||||
The example was dumped using this action:
|
||||
|
||||
```yaml
|
||||
- actions:
|
||||
- command: [/usr/bin/bash, -c, "jq . >&2"]
|
||||
match_channels: ['#tezrian']
|
||||
match_message: '^!test'
|
||||
```
|
||||
|
||||
## Rule examples
|
||||
|
||||
### Game death counter with dynamic name
|
||||
|
|
Loading…
Reference in a new issue