Add command action to execute arbitrary logic

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2021-01-10 22:15:57 +01:00
parent b24ca61b8e
commit 6d9484be2d
Signed by: luzifer
GPG key ID: 0066F03ED215AD7D
4 changed files with 132 additions and 12 deletions

71
action_script.go Normal file
View 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
})
}

View file

@ -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
View file

@ -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

View file

@ -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