mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-11-09 08:40:01 +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 {
|
type ruleAction struct {
|
||||||
Ban *string `yaml:"ban"`
|
Ban *string `json:"ban" yaml:"ban"`
|
||||||
CounterStep *int64 `yaml:"counter_step"`
|
Command []string `json:"command" yaml:"command"`
|
||||||
Counter *string `yaml:"counter"`
|
CounterStep *int64 `json:"counter_step" yaml:"counter_step"`
|
||||||
DeleteMessage *bool `yaml:"delete_message"`
|
Counter *string `json:"counter" yaml:"counter"`
|
||||||
Respond *string `yaml:"respond"`
|
DeleteMessage *bool `json:"delete_message" yaml:"delete_message"`
|
||||||
Timeout *time.Duration `yaml:"timeout"`
|
Respond *string `json:"respond" yaml:"respond"`
|
||||||
|
Timeout *time.Duration `json:"timeout" yaml:"timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig(filename string) error {
|
func loadConfig(filename string) error {
|
||||||
|
|
13
main.go
13
main.go
|
@ -14,12 +14,13 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cfg = struct {
|
cfg = struct {
|
||||||
Config string `flag:"config,c" default:"./config.yaml" description:"Location of configuration file"`
|
CommandTimeout time.Duration `flag:"command-timeout" default:"30s" description:"Timeout for command execution"`
|
||||||
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
Config string `flag:"config,c" default:"./config.yaml" description:"Location of configuration file"`
|
||||||
StorageFile string `flag:"storage-file" default:"./storage.json.gz" description:"Where to store the data"`
|
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
||||||
TwitchClient string `flag:"twitch-client" default:"" description:"Client ID to act as" validate:"nonzero"`
|
StorageFile string `flag:"storage-file" default:"./storage.json.gz" description:"Where to store the data"`
|
||||||
TwitchToken string `flag:"twitch-token" default:"" description:"OAuth token valid for client"`
|
TwitchClient string `flag:"twitch-client" default:"" description:"Client ID to act as" validate:"nonzero"`
|
||||||
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
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
|
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
|
# Issue a ban on the user who wrote the chat-line
|
||||||
- ban: "reason of ban"
|
- 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)
|
# Modify an internal counter value (does NOT send a chat line)
|
||||||
- counter: "counterid" # String to identify the counter, applies templating
|
- counter: "counterid" # String to identify the counter, applies templating
|
||||||
counter_step: 1 # Integer, can be negative or positive, default: +1
|
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.
|
- `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
|
- `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
|
## Rule examples
|
||||||
|
|
||||||
### Game death counter with dynamic name
|
### Game death counter with dynamic name
|
||||||
|
|
Loading…
Reference in a new issue