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.
This commit is contained in:
Chris Rhodes 2013-02-16 23:50:42 -08:00
parent 18a149335b
commit 89c23a7787
5 changed files with 143 additions and 112 deletions

View File

@ -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 <n>" 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
}

View File

@ -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 + "_" },

View File

@ -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)
}
}

View File

@ -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]

View File

@ -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)
}