// 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 }