From 44cfbb49b9192fe33102f73edcace06ae7928754 Mon Sep 17 00:00:00 2001 From: Sam Hofius Date: Sun, 24 Jan 2021 22:29:10 -0500 Subject: [PATCH] Add bot code --- go.mod | 10 ++ go.sum | 20 ++++ main.go | 281 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 311 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..96f5209 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6bad9f1 --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..5af17f1 --- /dev/null +++ b/main.go @@ -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 +(, , ... ) +%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: " ", + 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 +}