From 89c23a778709f48acd97156158e5dc388faaef23 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Sat, 16 Feb 2013 23:50:42 -0800 Subject: [PATCH] Add support for complex regex commands. Refactor commands so they are just handlers. Add commands that listen for YouTube/Webpage mentions and print information (Video Info/Title). Add simple command examples in client.go TODO: Use a better data store for the commands, perhaps a linked list like handlers. --- client.go | 35 ++++++++-- client/connection.go | 7 +- client/dispatch.go | 161 ++++++++++++++++++++++++++----------------- client/funcs.go | 8 ++- client/handlers.go | 44 +++--------- 5 files changed, 143 insertions(+), 112 deletions(-) diff --git a/client.go b/client.go index af65da9..8575564 100644 --- a/client.go +++ b/client.go @@ -5,18 +5,23 @@ import ( "flag" "fmt" irc "github.com/fluffle/goirc/client" + "math/rand" "os" + "strconv" "strings" ) -var host *string = flag.String("host", "irc.freenode.net", "IRC server") +var host *string = flag.String("host", "irc.synirc.net", "IRC server") var channel *string = flag.String("channel", "#go-nuts", "IRC channel") +var nick *string = flag.String("nick", "Septapus", "Nick") +var ident *string = flag.String("ident", "Septapus", "Ident") +var name *string = flag.String("name", "Septapus v9", "Name") func main() { flag.Parse() // create new IRC connection - c := irc.Client("GoTest", "gotest") + c := irc.Client(*nick, *ident, *name) c.EnableStateTracking() c.HandleFunc(irc.CONNECTED, func(conn *irc.Conn, line *irc.Line) { conn.Join(*channel) }) @@ -26,8 +31,27 @@ func main() { c.HandleFunc(irc.DISCONNECTED, func(conn *irc.Conn, line *irc.Line) { quit <- true }) - c.HandleFunc(irc.PRIVMSG, YouTubeFunc) - c.HandleFunc(irc.PRIVMSG, UrlFunc) + // Set up some simple commands, !bark and !roll. + c.SimpleCommandFunc("bark", func(conn *irc.Conn, line *irc.Line) { conn.Privmsg(line.Target(), "Woof Woof") }) + c.SimpleCommandHelpFunc("roll", `Rolls a d6, "roll " to roll n dice at once.`, func(conn *irc.Conn, line *irc.Line) { + count := 1 + fields := strings.Fields(line.Message()) + if len(fields) > 1 { + var err error + if count, err = strconv.Atoi(fields[len(fields)-1]); err != nil { + count = 1 + } + } + total := 0 + for i := 0; i < count; i++ { + total += rand.Intn(6) + 1 + } + conn.Privmsg(line.Target(), fmt.Sprintf("%d", total)) + }) + + // Set up some commands that are triggered by a regex in a message. + c.CommandFunc(irc.YouTubeRegex, irc.YouTubeFunc, 10) + c.CommandFunc(irc.UrlRegex, irc.UrlFunc, 0) // set up a goroutine to read commands from stdin in := make(chan string, 4) @@ -87,10 +111,9 @@ func main() { for !reallyquit { // connect to server if err := c.Connect(*host); err != nil { - fmt.Printf("Connection error: %s\n", err) + fmt.Printf("Error %v", err) return } - // wait on quit channel <-quit } diff --git a/client/connection.go b/client/connection.go index 1cd0ece..b9ea58c 100644 --- a/client/connection.go +++ b/client/connection.go @@ -22,7 +22,7 @@ type Conn struct { // Handlers and Commands handlers *hSet - commands *cSet + commands *commandSet // State tracker for nicks and channels ST state.StateTracker @@ -54,9 +54,6 @@ type Conn struct { // Client->server ping frequency, in seconds. Defaults to 3m. PingFreq time.Duration - // Controls what is stripped from line.Args[1] for Commands - CommandStripNick, CommandStripPrefix bool - // Set this to true to disable flood protection and false to re-enable Flood bool @@ -85,7 +82,7 @@ func Client(nick string, args ...string) *Conn { cLoop: make(chan bool), cPing: make(chan bool), handlers: handlerSet(), - commands: commandSet(), + commands: newCommandSet(), stRemovers: make([]Remover, 0, len(stHandlers)), PingFreq: 3 * time.Minute, NewNick: func(s string) string { return s + "_" }, diff --git a/client/dispatch.go b/client/dispatch.go index dd73865..816c126 100644 --- a/client/dispatch.go +++ b/client/dispatch.go @@ -1,7 +1,10 @@ package client import ( + "fmt" "github.com/fluffle/golog/logging" + "math" + "regexp" "strings" "sync" ) @@ -16,6 +19,12 @@ type Remover interface { Remove() } +type RemoverFunc func() + +func (r RemoverFunc) Remove() { + r() +} + type HandlerFunc func(*Conn, *Line) func (hf HandlerFunc) Handle(conn *Conn, line *Line) { @@ -113,87 +122,76 @@ func (hs *hSet) dispatch(conn *Conn, line *Line) { } } -// An IRC command looks like this: -type Command interface { - Execute(*Conn, *Line) - Help() string -} - type command struct { - fn HandlerFunc - help string + handler Handler + set *commandSet + regex string + priority int } -func (c *command) Execute(conn *Conn, line *Line) { - c.fn(conn, line) +func (c *command) Handle(conn *Conn, line *Line) { + c.handler.Handle(conn, line) } -func (c *command) Help() string { - return c.help +func (c *command) Remove() { + c.set.remove(c) } -type cNode struct { - cmd Command - set *cSet - prefix string -} - -func (cn *cNode) Execute(conn *Conn, line *Line) { - cn.cmd.Execute(conn, line) -} - -func (cn *cNode) Help() string { - return cn.cmd.Help() -} - -func (cn *cNode) Remove() { - cn.set.remove(cn) -} - -type cSet struct { - set map[string]*cNode +type commandSet struct { + set []*command sync.RWMutex } -func commandSet() *cSet { - return &cSet{set: make(map[string]*cNode)} +func newCommandSet() *commandSet { + return &commandSet{} } -func (cs *cSet) add(pf string, c Command) Remover { +func (cs *commandSet) add(regex string, handler Handler, priority int) Remover { cs.Lock() defer cs.Unlock() - pf = strings.ToLower(pf) - if _, ok := cs.set[pf]; ok { - logging.Error("Command prefix '%s' already registered.", pf) - return nil + c := &command{ + handler: handler, + set: cs, + regex: regex, + priority: priority, } - cn := &cNode{ - cmd: c, - set: cs, - prefix: pf, + // Check for exact regex matches. This will filter out any repeated SimpleCommands. + for _, c := range cs.set { + if c.regex == regex { + logging.Error("Command prefix '%s' already registered.", regex) + return nil + } } - cs.set[pf] = cn - return cn + cs.set = append(cs.set, c) + return c } -func (cs *cSet) remove(cn *cNode) { +func (cs *commandSet) remove(c *command) { cs.Lock() defer cs.Unlock() - delete(cs.set, cn.prefix) - cn.set = nil + for index, value := range cs.set { + if value == c { + copy(cs.set[index:], cs.set[index+1:]) + cs.set = cs.set[:len(cs.set)-1] + c.set = nil + return + } + } } -func (cs *cSet) match(txt string) (final Command, prefixlen int) { +// Matches the command with the highest priority. +func (cs *commandSet) match(txt string) (handler Handler) { cs.RLock() defer cs.RUnlock() - txt = strings.ToLower(txt) - for prefix, cmd := range cs.set { - if !strings.HasPrefix(txt, prefix) { - continue - } - if final == nil || len(prefix) > prefixlen { - prefixlen = len(prefix) - final = cmd + maxPriority := math.MinInt32 + for _, c := range cs.set { + if c.priority > maxPriority { + if regex, error := regexp.Compile(c.regex); error == nil { + if regex.MatchString(txt) { + maxPriority = c.priority + handler = c.handler + } + } } } return @@ -213,18 +211,55 @@ func (conn *Conn) HandleFunc(name string, hf HandlerFunc) Remover { return conn.Handle(name, hf) } -func (conn *Conn) Command(prefix string, c Command) Remover { - return conn.commands.add(prefix, c) +func (conn *Conn) Command(regex string, handler Handler, priority int) Remover { + return conn.commands.add(regex, handler, priority) } -func (conn *Conn) CommandFunc(prefix string, hf HandlerFunc, help string) Remover { - return conn.Command(prefix, &command{hf, help}) +func (conn *Conn) CommandFunc(regex string, handlerFunc HandlerFunc, priority int) Remover { + return conn.Command(regex, handlerFunc, priority) +} + +var SimpleCommandRegex string = `^!%v(\s|$)` + +// Simple commands are commands that are triggered from a simple prefix +// SimpleCommand("roll" handler) +// !roll +// Because simple commands are simple, they get the highest priority. +func (conn *Conn) SimpleCommand(prefix string, handler Handler) Remover { + return conn.Command(fmt.Sprintf(SimpleCommandRegex, strings.ToLower(prefix)), handler, math.MaxInt32) +} + +func (conn *Conn) SimpleCommandFunc(prefix string, handlerFunc HandlerFunc) Remover { + return conn.SimpleCommand(prefix, handlerFunc) +} + +// This will also register a help command to go along with the simple command itself. +// eg. SimpleCommandHelp("bark", "Bot will bark", handler) will make the following commands: +// !bark +// !help bark +func (conn *Conn) SimpleCommandHelp(prefix string, help string, handler Handler) Remover { + commandCommand := conn.SimpleCommand(prefix, handler) + helpCommand := conn.SimpleCommandFunc(fmt.Sprintf("help %v", prefix), HandlerFunc(func(conn *Conn, line *Line) { + conn.Privmsg(line.Target(), help) + })) + return RemoverFunc(func() { + commandCommand.Remove() + helpCommand.Remove() + }) +} + +func (conn *Conn) SimpleCommandHelpFunc(prefix string, help string, handlerFunc HandlerFunc) Remover { + return conn.SimpleCommandHelp(prefix, help, handlerFunc) } func (conn *Conn) dispatch(line *Line) { conn.handlers.dispatch(conn, line) } -func (conn *Conn) cmdMatch(txt string) (Command, int) { - return conn.commands.match(txt) +func (conn *Conn) command(line *Line) { + command := conn.commands.match(strings.ToLower(line.Message())) + if command != nil { + command.Handle(conn, line) + } + } diff --git a/client/funcs.go b/client/funcs.go index 65b4164..0e8cec9 100644 --- a/client/funcs.go +++ b/client/funcs.go @@ -29,9 +29,11 @@ type youTubeVideo struct { } `json:entry` } +var UrlRegex string = `(\s|^)(http://|https://)(.*?)(\s|$)` + func UrlFunc(conn *Conn, line *Line) { text := line.Message() - if regex, err := regexp.Compile(`(\s|^)(http://|https://)(.*?)(\s|$)`); err == nil { + if regex, err := regexp.Compile(UrlRegex); err == nil { url := strings.TrimSpace(regex.FindString(text)) if url != "" { if resp, err := http.Get(url); err == nil { @@ -50,9 +52,11 @@ func UrlFunc(conn *Conn, line *Line) { } } +var YouTubeRegex string = `(\s|^)(http://|https://)?(www.)?(youtube.com/watch\?v=|youtu.be/)(.*?)(\s|$|\&|#)` + func YouTubeFunc(conn *Conn, line *Line) { text := line.Message() - if regex, err := regexp.Compile(`(\s|^)(http://|https://)?(www.)?(youtube.com/watch\?v=|youtu.be/)(.*?)(\s|$|\&|#)`); err == nil { + if regex, err := regexp.Compile(YouTubeRegex); err == nil { if regex.Match([]byte(text)) { matches := regex.FindStringSubmatch(text) id := matches[len(matches)-2] diff --git a/client/handlers.go b/client/handlers.go index 5d017be..f5ded17 100644 --- a/client/handlers.go +++ b/client/handlers.go @@ -9,12 +9,13 @@ import ( // sets up the internal event handlers to do essential IRC protocol things var intHandlers = map[string]HandlerFunc{ - INIT: (*Conn).h_init, - "001": (*Conn).h_001, - "433": (*Conn).h_433, - CTCP: (*Conn).h_CTCP, - NICK: (*Conn).h_NICK, - PING: (*Conn).h_PING, + INIT: (*Conn).h_init, + "001": (*Conn).h_001, + "433": (*Conn).h_433, + CTCP: (*Conn).h_CTCP, + NICK: (*Conn).h_NICK, + PING: (*Conn).h_PING, + PRIVMSG: (*Conn).h_PRIVMSG, } func (conn *Conn) addIntHandlers() { @@ -96,34 +97,5 @@ func (conn *Conn) h_NICK(line *Line) { // Handle PRIVMSGs that trigger Commands func (conn *Conn) h_PRIVMSG(line *Line) { - txt := line.Args[1] - if conn.CommandStripNick && strings.HasPrefix(txt, conn.Me.Nick) { - // Look for '^${nick}[:;>,-]? ' - l := len(conn.Me.Nick) - switch txt[l] { - case ':', ';', '>', ',', '-': - l++ - } - if txt[l] == ' ' { - txt = strings.TrimSpace(txt[l:]) - } - } - cmd, l := conn.cmdMatch(txt) - if cmd == nil { - return - } - if conn.CommandStripPrefix { - txt = strings.TrimSpace(txt[l:]) - } - if txt != line.Args[1] { - line = line.Copy() - line.Args[1] = txt - } - cmd.Execute(conn, line) -} - -func (conn *Conn) c_HELP(line *Line) { - if cmd, _ := conn.cmdMatch(line.Args[1]); cmd != nil { - conn.Privmsg(line.Args[0], cmd.Help()) - } + conn.command(line) }