goirc/client/dispatch.go

233 lines
5.6 KiB
Go

package client
import (
"container/list"
"fmt"
"github.com/fluffle/golog/logging"
"math"
"regexp"
"strings"
"sync"
)
// An IRC handler looks like this:
type Handler interface {
Handle(*Conn, *Line)
}
type HandlerFunc func(*Conn, *Line)
func (hf HandlerFunc) Handle(conn *Conn, line *Line) {
hf(conn, line)
}
// And when they've been added to the client they are removable.
type Remover interface {
Remove()
}
type RemoverFunc func()
func (r RemoverFunc) Remove() {
r()
}
type handlerElement struct {
event string
handler Handler
}
type handlerSet struct {
set map[string]*list.List
sync.RWMutex
}
func newHandlerSet() *handlerSet {
return &handlerSet{set: make(map[string]*list.List)}
}
func (hs *handlerSet) add(event string, handler Handler) Remover {
hs.Lock()
defer hs.Unlock()
event = strings.ToLower(event)
l, ok := hs.set[event]
if !ok {
l = list.New()
hs.set[event] = l
}
element := l.PushBack(&handlerElement{event, handler})
return RemoverFunc(func() {
hs.remove(element)
})
}
func (hs *handlerSet) remove(element *list.Element) {
hs.Lock()
defer hs.Unlock()
h := element.Value.(*handlerElement)
l, ok := hs.set[h.event]
if !ok {
logging.Error("Removing node for unknown event '%s'", h.event)
return
}
l.Remove(element)
if l.Len() == 0 {
delete(hs.set, h.event)
}
}
func (hs *handlerSet) dispatch(conn *Conn, line *Line) {
hs.RLock()
defer hs.RUnlock()
event := strings.ToLower(line.Cmd)
l, ok := hs.set[event]
if !ok {
return
}
for e := l.Front(); e != nil; e = e.Next() {
h := e.Value.(*handlerElement)
go h.handler.Handle(conn, line)
}
}
type commandElement struct {
regex string
handler Handler
priority int
}
type commandList struct {
list *list.List
sync.RWMutex
}
func newCommandList() *commandList {
return &commandList{list: list.New()}
}
func (cl *commandList) add(regex string, handler Handler, priority int) Remover {
cl.Lock()
defer cl.Unlock()
c := &commandElement{
regex: regex,
handler: handler,
priority: priority,
}
// Check for exact regex matches. This will filter out any repeated SimpleCommands.
for e := cl.list.Front(); e != nil; e = e.Next() {
c := e.Value.(*commandElement)
if c.regex == regex {
logging.Error("Command prefix '%s' already registered.", regex)
return nil
}
}
element := cl.list.PushBack(c)
return RemoverFunc(func() {
cl.remove(element)
})
}
func (cl *commandList) remove(element *list.Element) {
cl.Lock()
defer cl.Unlock()
cl.list.Remove(element)
}
// Matches the command with the highest priority.
func (cl *commandList) match(text string) (handler Handler) {
cl.RLock()
defer cl.RUnlock()
maxPriority := math.MinInt32
text = strings.ToLower(text)
for e := cl.list.Front(); e != nil; e = e.Next() {
c := e.Value.(*commandElement)
if c.priority > maxPriority {
if regex, error := regexp.Compile(c.regex); error == nil {
if regex.MatchString(text) {
maxPriority = c.priority
handler = c.handler
}
}
}
}
return
}
// Handlers are triggered on incoming Lines from the server, with the handler
// "name" being equivalent to Line.Cmd. Read the RFCs for details on what
// replies could come from the server. They'll generally be things like
// "PRIVMSG", "JOIN", etc. but all the numeric replies are left as ascii
// strings of digits like "332" (mainly because I really didn't feel like
// putting massive constant tables in).
func (conn *Conn) Handle(name string, handler Handler) Remover {
return conn.handlers.add(name, handler)
}
func (conn *Conn) HandleFunc(name string, handlerFunc HandlerFunc) Remover {
return conn.Handle(name, handlerFunc)
}
func (conn *Conn) Command(regex string, handler Handler, priority int) Remover {
return conn.commands.add(regex, handler, priority)
}
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 {
stripHandler := func(conn *Conn, line *Line) {
text := line.Message()
if conn.cfg.SimpleCommandStripPrefix {
text = strings.TrimSpace(text[len(prefix):])
}
if text != line.Message() {
line = line.Copy()
line.Args[1] = text
}
handler.Handle(conn, line)
}
return conn.Command(fmt.Sprintf(SimpleCommandRegex, strings.ToLower(prefix)), HandlerFunc(stripHandler), 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) command(line *Line) {
command := conn.commands.match(line.Message())
if command != nil {
go command.Handle(conn, line)
}
}