3 changed files with 311 additions and 0 deletions
@ -0,0 +1,10 @@ |
|||||||
|
module git.hugfreevikings.wtf/bots/macro |
||||||
|
|
||||||
|
go 1.15 |
||||||
|
|
||||||
|
require ( |
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 |
||||||
|
github.com/kf5grd/keybasebot v1.7.0 |
||||||
|
github.com/urfave/cli/v2 v2.3.0 |
||||||
|
samhofi.us/x/keybase/v2 v2.1.1 |
||||||
|
) |
@ -0,0 +1,20 @@ |
|||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= |
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= |
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= |
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= |
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= |
||||||
|
github.com/kf5grd/keybasebot v1.7.0 h1:8Jl6RSYEIyzniZ7UFyH/fjTlsM0tZHTCZ5F8bq8JTDA= |
||||||
|
github.com/kf5grd/keybasebot v1.7.0/go.mod h1:8T07cWZZrl2G6hTRsL9x2SBwaH8gEZocF9NRknSU3dY= |
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
||||||
|
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= |
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= |
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= |
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= |
||||||
|
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= |
||||||
|
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= |
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||||
|
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||||
|
samhofi.us/x/keybase/v2 v2.0.8/go.mod h1:lJivwhzMSV+WUg+XUbatszStjjFVcuLGl+xcQpqQ5GQ= |
||||||
|
samhofi.us/x/keybase/v2 v2.1.1 h1:XPWrmdbJCrNcsW3sRuR6WuALYOZt7O+av0My6YoehqE= |
||||||
|
samhofi.us/x/keybase/v2 v2.1.1/go.mod h1:lJivwhzMSV+WUg+XUbatszStjjFVcuLGl+xcQpqQ5GQ= |
@ -0,0 +1,281 @@ |
|||||||
|
// This is a very simple bot that has 2 commands: set, and get. The set command sets a
|
||||||
|
// string variable named "message" in the Meta store, and the get command retrieves that
|
||||||
|
// variable and sends it to the user in a chat message.
|
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/google/shlex" |
||||||
|
"github.com/urfave/cli/v2" |
||||||
|
|
||||||
|
bot "github.com/kf5grd/keybasebot" |
||||||
|
"github.com/kf5grd/keybasebot/pkg/util" |
||||||
|
"samhofi.us/x/keybase/v2" |
||||||
|
"samhofi.us/x/keybase/v2/types/chat1" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
back = "`" |
||||||
|
backs = "```" |
||||||
|
) |
||||||
|
|
||||||
|
type Command struct { |
||||||
|
Name string |
||||||
|
Args []string |
||||||
|
Response string |
||||||
|
} |
||||||
|
|
||||||
|
// Current version
|
||||||
|
var version string |
||||||
|
|
||||||
|
// Exit code on failure
|
||||||
|
const exitFail = 1 |
||||||
|
|
||||||
|
func main() { |
||||||
|
app := cli.App{ |
||||||
|
Name: "funcy", |
||||||
|
Usage: "Go interpreter for Keybase", |
||||||
|
Version: version, |
||||||
|
Writer: os.Stdout, |
||||||
|
EnableBashCompletion: true, |
||||||
|
Action: run, |
||||||
|
Flags: []cli.Flag{ |
||||||
|
&cli.PathFlag{ |
||||||
|
Name: "home", |
||||||
|
Aliases: []string{"H"}, |
||||||
|
Usage: "Keybase Home Folder", |
||||||
|
EnvVars: []string{"MACROBOT_HOME"}, |
||||||
|
}, |
||||||
|
&cli.StringFlag{ |
||||||
|
Name: "bot-owner", |
||||||
|
Usage: "Username of the bot owner", |
||||||
|
EnvVars: []string{"MACROBOT_OWNER"}, |
||||||
|
}, |
||||||
|
&cli.StringFlag{ |
||||||
|
Name: "log-conv", |
||||||
|
Usage: "Conversation ID to send logs to", |
||||||
|
EnvVars: []string{"MACROBOT_LOGCONV"}, |
||||||
|
}, |
||||||
|
&cli.BoolFlag{ |
||||||
|
Name: "debug", |
||||||
|
Aliases: []string{"d"}, |
||||||
|
Usage: "Enable extra log output", |
||||||
|
EnvVars: []string{"MACROBOT_DEBUG"}, |
||||||
|
}, |
||||||
|
&cli.BoolFlag{ |
||||||
|
Name: "json", |
||||||
|
Aliases: []string{"j"}, |
||||||
|
Usage: "Output log in JSON format", |
||||||
|
EnvVars: []string{"MACROBOT_JSON"}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil { |
||||||
|
os.Exit(exitFail) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func run(c *cli.Context) error { |
||||||
|
// setup bot
|
||||||
|
b := bot.New("", keybase.SetHomePath(c.Path("home"))) |
||||||
|
b.Debug = c.Bool("debug") |
||||||
|
b.JSON = c.Bool("json") |
||||||
|
b.LogConv = chat1.ConvIDStr(c.String("log-conv")) |
||||||
|
b.LogWriter = os.Stdout |
||||||
|
|
||||||
|
// register the bot's commands
|
||||||
|
b.Commands = append(b.Commands, |
||||||
|
bot.BotCommand{ |
||||||
|
Name: "CreateCommand", |
||||||
|
Ad: adCreateCommand(), |
||||||
|
Run: bot.Adapt(cmdCreateCommand, |
||||||
|
bot.MessageType("text"), |
||||||
|
bot.CommandPrefix("!create"), |
||||||
|
), |
||||||
|
}, |
||||||
|
) |
||||||
|
|
||||||
|
// run bot
|
||||||
|
b.Run() |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func adCreateCommand() *chat1.UserBotCommandInput { |
||||||
|
var createDesc = fmt.Sprintf(`Commands should be in the following format, and should be followed by the response: |
||||||
|
%s |
||||||
|
<command>(<arg name 1>, <arg name 2>, ... <arg name n>) |
||||||
|
%s |
||||||
|
|
||||||
|
You can reference the arguments in the response by prepending the argument name with a %s$%s |
||||||
|
|
||||||
|
Here's an example command that can be used to tell the bot to say "Hello" to a specific person: |
||||||
|
%s |
||||||
|
!create sayhi(user) Hello, @$user! |
||||||
|
%s |
||||||
|
`, backs, backs, back, back, backs, backs) |
||||||
|
|
||||||
|
return &chat1.UserBotCommandInput{ |
||||||
|
Name: "create", |
||||||
|
Usage: "<command> <response>", |
||||||
|
Description: "Create a new command", |
||||||
|
ExtendedDescription: &chat1.UserBotExtendedDescription{ |
||||||
|
Title: "Create A New Command", |
||||||
|
DesktopBody: createDesc, |
||||||
|
MobileBody: createDesc, |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func cmdCreateCommand(m chat1.MsgSummary, b *bot.Bot) (bool, error) { |
||||||
|
body := m.Content.Text.Body |
||||||
|
fields := strings.Fields(body) |
||||||
|
if len(fields) < 3 { |
||||||
|
return true, fmt.Errorf("Must provide both command and response") |
||||||
|
} |
||||||
|
|
||||||
|
command, err := parseCommandDef(strings.Replace(body, "!create", "", 1)) |
||||||
|
if err != nil { |
||||||
|
return true, err |
||||||
|
} |
||||||
|
|
||||||
|
metaIndex := fmt.Sprintf("%s-%s", m.ConvID, command.Name) |
||||||
|
if _, ok := b.Meta[metaIndex]; ok { |
||||||
|
return true, fmt.Errorf("That command already exists") |
||||||
|
} |
||||||
|
|
||||||
|
responsePrefix := strings.Fields(command.Response)[0] |
||||||
|
if strings.HasPrefix("/", responsePrefix) && !util.StringInSlice(responsePrefix, []string{"/giphy", "/flip", "/me", "/shrug"}) { |
||||||
|
return true, fmt.Errorf("The only `/` commands allowed are `/giphy`, `/flip`, `/me`, and `/shrug`. Nice try though!") |
||||||
|
} |
||||||
|
|
||||||
|
for _, c := range b.Commands { |
||||||
|
if command.Name == strings.ToLower(c.Ad.Name) && c.AdType != "conv" { |
||||||
|
return true, fmt.Errorf("You cannot override an existing public command") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var usageString string |
||||||
|
if len(command.Args) > 0 { |
||||||
|
for _, arg := range command.Args { |
||||||
|
usageString = fmt.Sprintf("%s <%s>", usageString, arg) |
||||||
|
} |
||||||
|
usageString = strings.TrimSpace(usageString) |
||||||
|
} |
||||||
|
|
||||||
|
b.Meta[metaIndex] = command.Response |
||||||
|
b.Commands = append(b.Commands, bot.BotCommand{ |
||||||
|
Name: fmt.Sprintf("Custom [%s]", metaIndex), |
||||||
|
AdType: "conv", |
||||||
|
AdConv: m.ConvID, |
||||||
|
Ad: &chat1.UserBotCommandInput{ |
||||||
|
Name: command.Name, |
||||||
|
Usage: usageString, |
||||||
|
Description: command.Response, |
||||||
|
}, |
||||||
|
Run: bot.Adapt(cmdCommand(command), |
||||||
|
bot.MessageType("text"), |
||||||
|
bot.CommandPrefix("!"+command.Name), |
||||||
|
), |
||||||
|
}) |
||||||
|
b.AdvertiseCommands() |
||||||
|
|
||||||
|
b.KB.ReactByConvID(m.ConvID, m.Id, ":heavy_check_mark:") |
||||||
|
return true, nil |
||||||
|
} |
||||||
|
|
||||||
|
func cmdCommand(cmd Command) bot.BotAction { |
||||||
|
return func(m chat1.MsgSummary, b *bot.Bot) (bool, error) { |
||||||
|
argText := strings.Replace(m.Content.Text.Body, "!", "", 1) |
||||||
|
argText = strings.TrimSpace(argText) |
||||||
|
|
||||||
|
a, _ := shlex.Split(argText) |
||||||
|
var receivedArgs []string |
||||||
|
command := a[0] |
||||||
|
if len(a) > 1 { |
||||||
|
receivedArgs = a[1:] |
||||||
|
} |
||||||
|
|
||||||
|
if cmd.Name != command { |
||||||
|
return false, nil |
||||||
|
} |
||||||
|
|
||||||
|
metaIndex := fmt.Sprintf("%s-%s", m.ConvID, cmd.Name) |
||||||
|
response, ok := b.Meta[metaIndex] |
||||||
|
if !ok { |
||||||
|
return false, nil |
||||||
|
} |
||||||
|
|
||||||
|
if len(receivedArgs) != len(cmd.Args) { |
||||||
|
return true, fmt.Errorf("Incorrect number of arguments supplied") |
||||||
|
} |
||||||
|
|
||||||
|
b.Logger.Debug("cmd.Name: %s, cmd.Args: %s, argText: %s, receivedArgs: %s", cmd.Name, cmd.Args, argText, receivedArgs) |
||||||
|
|
||||||
|
cleanResponse := response.(string) |
||||||
|
cleanResponse = strings.ReplaceAll(cleanResponse, "%", "%%") |
||||||
|
cleanResponse = strings.ReplaceAll(cleanResponse, "$USER", m.Sender.Username) |
||||||
|
|
||||||
|
for argIndex, arg := range cmd.Args { |
||||||
|
cleanResponse = strings.ReplaceAll(cleanResponse, "$"+strings.TrimSpace(arg), receivedArgs[argIndex]) |
||||||
|
} |
||||||
|
|
||||||
|
b.KB.SendMessageByConvID(m.ConvID, cleanResponse) |
||||||
|
return true, nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func parseCommandDef(def string) (Command, error) { |
||||||
|
// def: funcName(arg1, arg2) response goes here
|
||||||
|
def = strings.TrimSpace(def) |
||||||
|
|
||||||
|
argStart := strings.Index(def, "(") + 1 |
||||||
|
argEnd := strings.Index(def, ")") |
||||||
|
|
||||||
|
if argStart == -1 || argEnd == -1 { |
||||||
|
return Command{}, fmt.Errorf("Invalid command definition") |
||||||
|
} |
||||||
|
|
||||||
|
name := strings.ToLower(strings.TrimSpace(def[0 : argStart-1])) |
||||||
|
|
||||||
|
args := strings.Split(def[argStart:argEnd], ",") |
||||||
|
args = trimArgs(args) |
||||||
|
|
||||||
|
response := strings.TrimSpace(def[argEnd+1:]) |
||||||
|
|
||||||
|
if len(args) > 0 { |
||||||
|
missing := make([]string, 0) |
||||||
|
for _, arg := range args { |
||||||
|
if !strings.Contains(response, "$"+arg) { |
||||||
|
missing = append(missing, arg) |
||||||
|
} |
||||||
|
} |
||||||
|
if len(missing) > 0 { |
||||||
|
return Command{}, fmt.Errorf("The following variable(s) were set in the command, but not referenced in the response: %s", strings.Join(missing, ", ")) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
ret := Command{ |
||||||
|
Name: name, |
||||||
|
Args: args, |
||||||
|
Response: response, |
||||||
|
} |
||||||
|
|
||||||
|
return ret, nil |
||||||
|
} |
||||||
|
|
||||||
|
func trimArgs(args []string) []string { |
||||||
|
newArgs := make([]string, 0) |
||||||
|
for _, arg := range args { |
||||||
|
trimmed := strings.TrimSpace(arg) |
||||||
|
if trimmed != "" { |
||||||
|
newArgs = append(newArgs, trimmed) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return newArgs |
||||||
|
} |
Loading…
Reference in new issue