package cmdhandler import ( "errors" "fmt" "regexp" "strings" "sync" "github.com/lrstanley/girc" ) // Input is a wrapper for events, based around private messages. type Input struct { Origin *girc.Event Args []string RawArgs string } // Command is an IRC command, supporting aliases, help documentation and easy // wrapping for message inputs. type Command struct { // Name of command, e.g. "search" or "ping". Name string // Aliases for the above command, e.g. "s" for search, or "p" for "ping". Aliases []string // Help documentation. Should be in the format " [arg] -- // something useful here" Help string // MinArgs is the minimum required arguments for the command. Defaults to // 0, which means multiple, or no arguments can be supplied. If set // above 0, this means that the command handler will throw an error asking // the person to check "help " for more info. MinArgs int // Fn is the function which is executed when the command is ran from a // private message, or channel. Fn func(*girc.Client, *Input) } func (c *Command) genHelp(prefix string) string { out := "{b}" + prefix + c.Name + "{b}" if c.Aliases != nil && len(c.Aliases) > 0 { out += " ({b}" + prefix + strings.Join(c.Aliases, "{b}, {b}"+prefix) + "{b})" } out += " :: " + c.Help return out } // CmdHandler is an irc command parser and execution format which you could // use as an example for building your own version/bot. // // An example of how you would register this with girc: // // ch, err := cmdhandler.New("!") // if err != nil { // panic(err) // } // // ch.Add(&cmdhandler.Command{ // Name: "ping", // Help: "Sends a pong reply back to the original user.", // Fn: func(c *girc.Client, input *cmdhandler.Input) { // c.Commands.ReplyTo(*input.Origin, "pong!") // }, // }) // // client.Handlers.AddHandler(girc.PRIVMSG, ch) type CmdHandler struct { prefix string re *regexp.Regexp mu sync.Mutex cmds map[string]*Command } var cmdMatch = `^%s([a-z0-9-_]{1,20})(?: (.*))?$` // New returns a new CmdHandler based on the specified command prefix. A good // prefix is a single character, and easy to remember/use. E.g. "!", or ".". func New(prefix string) (*CmdHandler, error) { re, err := regexp.Compile(fmt.Sprintf(cmdMatch, regexp.QuoteMeta(prefix))) if err != nil { return nil, err } return &CmdHandler{prefix: prefix, re: re, cmds: make(map[string]*Command)}, nil } var validName = regexp.MustCompile(`^[a-z0-9-_]{1,20}$`) // Add registers a new command to the handler. Note that you cannot remove // commands once added, unless you add another CmdHandler to the client. func (ch *CmdHandler) Add(cmd *Command) error { if cmd == nil { return errors.New("nil command provided to CmdHandler") } cmd.Name = strings.ToLower(cmd.Name) if !validName.MatchString(cmd.Name) { return fmt.Errorf("invalid command name: %q (req: %q)", cmd.Name, validName.String()) } if cmd.Aliases != nil { for i := 0; i < len(cmd.Aliases); i++ { cmd.Aliases[i] = strings.ToLower(cmd.Aliases[i]) if !validName.MatchString(cmd.Aliases[i]) { return fmt.Errorf("invalid command name: %q (req: %q)", cmd.Aliases[i], validName.String()) } } } if cmd.MinArgs < 0 { cmd.MinArgs = 0 } ch.mu.Lock() defer ch.mu.Unlock() if _, ok := ch.cmds[cmd.Name]; ok { return fmt.Errorf("command already registered: %s", cmd.Name) } ch.cmds[cmd.Name] = cmd // Since we'd be storing pointers, duplicates do not matter. for i := 0; i < len(cmd.Aliases); i++ { if _, ok := ch.cmds[cmd.Aliases[i]]; ok { return fmt.Errorf("alias already registered: %s", cmd.Aliases[i]) } ch.cmds[cmd.Aliases[i]] = cmd } return nil } // Execute satisfies the girc.Handler interface. func (ch *CmdHandler) Execute(client *girc.Client, event girc.Event) { if event.Source == nil || event.Command != girc.PRIVMSG { return } parsed := ch.re.FindStringSubmatch(event.Trailing) if len(parsed) != 3 { return } invCmd := strings.ToLower(parsed[1]) args := strings.Split(parsed[2], " ") if len(args) == 1 && args[0] == "" { args = []string{} } ch.mu.Lock() defer ch.mu.Unlock() if invCmd == "help" { if len(args) == 0 { client.Cmd.ReplyTo(event, girc.Fmt("type '{b}!help {blue}{c}{b}' to optionally get more info about a specific command.")) return } args[0] = strings.ToLower(args[0]) if _, ok := ch.cmds[args[0]]; !ok { client.Cmd.ReplyTof(event, girc.Fmt("unknown command {b}%q{b}."), args[0]) return } if ch.cmds[args[0]].Help == "" { client.Cmd.ReplyTof(event, girc.Fmt("there is no help documentation for {b}%q{b}"), args[0]) return } client.Cmd.ReplyTo(event, girc.Fmt(ch.cmds[args[0]].genHelp(ch.prefix))) return } cmd, ok := ch.cmds[invCmd] if !ok { return } if len(args) < cmd.MinArgs { client.Cmd.ReplyTof(event, girc.Fmt("not enough arguments supplied for {b}%q{b}. try '{b}%shelp %s{b}'?"), invCmd, ch.prefix, invCmd) return } in := &Input{ Origin: &event, Args: args, RawArgs: parsed[2], } go cmd.Fn(client, in) }