You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
278 lines
6.7 KiB
278 lines
6.7 KiB
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 |
|
}
|
|
|