From d467e2fa24b2ef8e6e0319b6ca63ae6486d796f8 Mon Sep 17 00:00:00 2001 From: Liam Stanley Date: Wed, 7 Jun 2017 06:53:25 -0400 Subject: [PATCH 1/3] begin example implementation of handling commands --- cmdhandler/cmd.go | 139 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 cmdhandler/cmd.go diff --git a/cmdhandler/cmd.go b/cmdhandler/cmd.go new file mode 100644 index 0000000..952e3fb --- /dev/null +++ b/cmdhandler/cmd.go @@ -0,0 +1,139 @@ +package cmdhandler + +import ( + "errors" + "fmt" + "strings" + "sync" + + "regexp" + + "github.com/lrstanley/girc" +) + +// This may eventually get merged directly into girc in the future. + +type Input struct { + Origin *girc.Event + Args []string + + // TODO: utilize Event.Source and PRIVMSG's Params to lookup these to + // make it easier on the end user. + User *girc.User + Channel *girc.Channel +} + +type Command struct { + Help string + MinArgs int + Fn func(*girc.Client, *Input) +} + +type CmdHandler struct { + prefix string + re *regexp.Regexp + + mu sync.Mutex + cmds map[string]*Command +} + +var cmdMatch = `^%s([a-z0-9-_]{1,20})(?: (.*))?$` + +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-zA-Z0-9-_]{1,20}$`) + +func (ch *CmdHandler) Add(name string, cmd *Command) error { + if cmd == nil { + return errors.New("nil command provided to CmdHandler") + } + + name = strings.ToLower(name) + + if !validName.MatchString(name) { + return fmt.Errorf("invalid command name: %q (req: %q)", name, validName.String()) + } + + if cmd.MinArgs < 0 { + cmd.MinArgs = 0 + } + + ch.mu.Lock() + defer ch.mu.Unlock() + + if _, ok := ch.cmds[name]; ok { + return fmt.Errorf("command already registered: %s", name) + } + + ch.cmds[name] = 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.Commands.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.Commands.ReplyTof(event, girc.Fmt("unknown command {b}%q{b}."), args[0]) + return + } + + if ch.cmds[args[0]].Help == "" { + client.Commands.ReplyTof(event, girc.Fmt("there is no help documentaiton for {b}%q{b}"), args[0]) + return + } + + client.Commands.ReplyTof(event, girc.Fmt("{b}%s%s{b} :: "+ch.cmds[args[0]].Help), ch.prefix, args[0]) + return + } + + cmd, ok := ch.cmds[invCmd] + if !ok { + // TODO: return "no command found" if config option set to do so? + return + } + + if len(args) < cmd.MinArgs { + client.Commands.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, + } + + go cmd.Fn(client, in) +} From 796deed9a65df133ae0f5ff05bf4cfa51a6b2982 Mon Sep 17 00:00:00 2001 From: Liam Stanley Date: Wed, 7 Jun 2017 06:59:31 -0400 Subject: [PATCH 2/3] add missing documentation --- commands.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/commands.go b/commands.go index 6d02df2..7bd4e54 100644 --- a/commands.go +++ b/commands.go @@ -140,6 +140,9 @@ func (cmd *Commands) Messagef(target, format string, a ...interface{}) error { return cmd.Message(target, fmt.Sprintf(format, a...)) } +// ErrInvalidSource is returned when a method needs to know the origin of an +// event, however Event.Source is unknown (e.g. sent by the user, not the +// server.) var ErrInvalidSource = errors.New("event has nil or invalid source address") // Reply sends a reply to channel or user, based on where the supplied event From 3eb923d88cba197f94df7eb2d892f935c5649dbf Mon Sep 17 00:00:00 2001 From: Liam Stanley Date: Fri, 9 Jun 2017 18:33:55 -0400 Subject: [PATCH 3/3] clean up cmdhandler a bit --- cmdhandler/cmd.go | 100 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 79 insertions(+), 21 deletions(-) diff --git a/cmdhandler/cmd.go b/cmdhandler/cmd.go index 952e3fb..3f8486f 100644 --- a/cmdhandler/cmd.go +++ b/cmdhandler/cmd.go @@ -3,32 +3,70 @@ package cmdhandler import ( "errors" "fmt" + "regexp" "strings" "sync" - "regexp" - "github.com/lrstanley/girc" ) -// This may eventually get merged directly into girc in the future. - +// Input is a wrapper for events, based around private messages. type Input struct { Origin *girc.Event Args []string - - // TODO: utilize Event.Source and PRIVMSG's Params to lookup these to - // make it easier on the end user. - User *girc.User - Channel *girc.Channel } +// Command is an IRC command, supporting aliases, help documentation and easy +// wrapping for message inputs. type Command struct { - Help string + // 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 func(*girc.Client, *Input) + // 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 @@ -39,6 +77,8 @@ type CmdHandler struct { 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 { @@ -48,17 +88,27 @@ func New(prefix string) (*CmdHandler, error) { return &CmdHandler{prefix: prefix, re: re, cmds: make(map[string]*Command)}, nil } -var validName = regexp.MustCompile(`^[a-zA-Z0-9-_]{1,20}$`) +var validName = regexp.MustCompile(`^[a-z0-9-_]{1,20}$`) -func (ch *CmdHandler) Add(name string, cmd *Command) error { +// 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") } - name = strings.ToLower(name) + 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 !validName.MatchString(name) { - return fmt.Errorf("invalid command name: %q (req: %q)", 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 { @@ -68,11 +118,20 @@ func (ch *CmdHandler) Add(name string, cmd *Command) error { ch.mu.Lock() defer ch.mu.Unlock() - if _, ok := ch.cmds[name]; ok { - return fmt.Errorf("command already registered: %s", name) + if _, ok := ch.cmds[cmd.Name]; ok { + return fmt.Errorf("command already registered: %s", cmd.Name) } - ch.cmds[name] = cmd + 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 } @@ -115,13 +174,12 @@ func (ch *CmdHandler) Execute(client *girc.Client, event girc.Event) { return } - client.Commands.ReplyTof(event, girc.Fmt("{b}%s%s{b} :: "+ch.cmds[args[0]].Help), ch.prefix, args[0]) + client.Commands.ReplyTo(event, girc.Fmt(ch.cmds[args[0]].genHelp(ch.prefix))) return } cmd, ok := ch.cmds[invCmd] if !ok { - // TODO: return "no command found" if config option set to do so? return }