This commit is contained in:
Chris Rhodes 2013-02-18 17:34:11 -08:00
commit be360c368d
12 changed files with 418 additions and 327 deletions

View File

@ -28,11 +28,11 @@ Synopsis:
// Add handlers to do things here! // Add handlers to do things here!
// e.g. join a channel on connect. // e.g. join a channel on connect.
c.HandleFunc("connected", c.HandleFunc(irc.CONNECTED,
func(conn *irc.Conn, line *irc.Line) { conn.Join("#channel") }) func(conn *irc.Conn, line *irc.Line) { conn.Join("#channel") })
// And a signal on disconnect // And a signal on disconnect
quit := make(chan bool) quit := make(chan bool)
c.HandleFunc("disconnected", c.HandleFunc(irc.DISCONNECTED,
func(conn *irc.Conn, line *irc.Line) { quit <- true }) func(conn *irc.Conn, line *irc.Line) { quit <- true })
// Tell client to connect. // Tell client to connect.

View File

@ -5,7 +5,9 @@ import (
"flag" "flag"
"fmt" "fmt"
irc "github.com/fluffle/goirc/client" irc "github.com/fluffle/goirc/client"
"math/rand"
"os" "os"
"strconv"
"strings" "strings"
) )
@ -18,14 +20,40 @@ func main() {
// create new IRC connection // create new IRC connection
c := irc.SimpleClient("GoTest", "gotest") c := irc.SimpleClient("GoTest", "gotest")
c.EnableStateTracking() c.EnableStateTracking()
c.HandleFunc("connected", c.HandleFunc(irc.CONNECTED,
func(conn *irc.Conn, line *irc.Line) { conn.Join(*channel) }) func(conn *irc.Conn, line *irc.Line) { conn.Join(*channel) })
// Set up a handler to notify of disconnect events. // Set up a handler to notify of disconnect events.
quit := make(chan bool) quit := make(chan bool)
c.HandleFunc("disconnected", c.HandleFunc(irc.DISCONNECTED,
func(conn *irc.Conn, line *irc.Line) { quit <- true }) func(conn *irc.Conn, line *irc.Line) { quit <- true })
// Set up some simple commands, !bark and !roll.
// The !roll command will also get the "!help roll" command also.
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.
// It is important to see that UrlRegex could actually respond to some
// of the Url's that YouTubeRegex listens to, because of this we put the
// YouTube command at a higher priority, this way it will take precedence.
c.CommandFunc(irc.YouTubeRegex, irc.YouTubeFunc, 10)
c.CommandFunc(irc.UrlRegex, irc.UrlFunc, 0)
// set up a goroutine to read commands from stdin // set up a goroutine to read commands from stdin
in := make(chan string, 4) in := make(chan string, 4)
reallyquit := false reallyquit := false
@ -36,6 +64,8 @@ func main() {
if err != nil { if err != nil {
// wha?, maybe ctrl-D... // wha?, maybe ctrl-D...
close(in) close(in)
reallyquit = true
c.Quit("")
break break
} }
// no point in sending empty lines down the channel // no point in sending empty lines down the channel
@ -85,7 +115,6 @@ func main() {
fmt.Printf("Connection error: %s\n", err) fmt.Printf("Connection error: %s\n", err)
return return
} }
// wait on quit channel // wait on quit channel
<-quit <-quit
} }

View File

@ -2,6 +2,34 @@ package client
import "strings" import "strings"
const (
REGISTER = "REGISTER"
CONNECTED = "CONNECTED"
DISCONNECTED = "DISCONNECTED"
ACTION = "ACTION"
AWAY = "AWAY"
CTCP = "CTCP"
CTCPREPLY = "CTCPREPLY"
INVITE = "INVITE"
JOIN = "JOIN"
KICK = "KICK"
MODE = "MODE"
NICK = "NICK"
NOTICE = "NOTICE"
OPER = "OPER"
PART = "PART"
PASS = "PASS"
PING = "PING"
PONG = "PONG"
PRIVMSG = "PRIVMSG"
QUIT = "QUIT"
TOPIC = "TOPIC"
USER = "USER"
VERSION = "VERSION"
WHO = "WHO"
WHOIS = "WHOIS"
)
// this file contains the various commands you can // this file contains the various commands you can
// send to the server using an Conn connection // send to the server using an Conn connection
@ -14,18 +42,18 @@ import "strings"
func (conn *Conn) Raw(rawline string) { conn.out <- rawline } func (conn *Conn) Raw(rawline string) { conn.out <- rawline }
// Pass() sends a PASS command to the server // Pass() sends a PASS command to the server
func (conn *Conn) Pass(password string) { conn.out <- "PASS " + password } func (conn *Conn) Pass(password string) { conn.out <- PASS + " " + password }
// Nick() sends a NICK command to the server // Nick() sends a NICK command to the server
func (conn *Conn) Nick(nick string) { conn.out <- "NICK " + nick } func (conn *Conn) Nick(nick string) { conn.out <- NICK + " " + nick }
// User() sends a USER command to the server // User() sends a USER command to the server
func (conn *Conn) User(ident, name string) { func (conn *Conn) User(ident, name string) {
conn.out <- "USER " + ident + " 12 * :" + name conn.out <- USER + " " + ident + " 12 * :" + name
} }
// Join() sends a JOIN command to the server // Join() sends a JOIN command to the server
func (conn *Conn) Join(channel string) { conn.out <- "JOIN " + channel } func (conn *Conn) Join(channel string) { conn.out <- JOIN + " " + channel }
// Part() sends a PART command to the server with an optional part message // Part() sends a PART command to the server with an optional part message
func (conn *Conn) Part(channel string, message ...string) { func (conn *Conn) Part(channel string, message ...string) {
@ -33,7 +61,7 @@ func (conn *Conn) Part(channel string, message ...string) {
if msg != "" { if msg != "" {
msg = " :" + msg msg = " :" + msg
} }
conn.out <- "PART " + channel + msg conn.out <- PART + " " + channel + msg
} }
// Kick() sends a KICK command to remove a nick from a channel // Kick() sends a KICK command to remove a nick from a channel
@ -42,7 +70,7 @@ func (conn *Conn) Kick(channel, nick string, message ...string) {
if msg != "" { if msg != "" {
msg = " :" + msg msg = " :" + msg
} }
conn.out <- "KICK " + channel + " " + nick + msg conn.out <- KICK + " " + channel + " " + nick + msg
} }
// Quit() sends a QUIT command to the server with an optional quit message // Quit() sends a QUIT command to the server with an optional quit message
@ -51,20 +79,20 @@ func (conn *Conn) Quit(message ...string) {
if msg == "" { if msg == "" {
msg = "GoBye!" msg = "GoBye!"
} }
conn.out <- "QUIT :" + msg conn.out <- QUIT + " :" + msg
} }
// Whois() sends a WHOIS command to the server // Whois() sends a WHOIS command to the server
func (conn *Conn) Whois(nick string) { conn.out <- "WHOIS " + nick } func (conn *Conn) Whois(nick string) { conn.out <- WHOIS + " " + nick }
//Who() sends a WHO command to the server //Who() sends a WHO command to the server
func (conn *Conn) Who(nick string) { conn.out <- "WHO " + nick } func (conn *Conn) Who(nick string) { conn.out <- WHO + " " + nick }
// Privmsg() sends a PRIVMSG to the target t // Privmsg() sends a PRIVMSG to the target t
func (conn *Conn) Privmsg(t, msg string) { conn.out <- "PRIVMSG " + t + " :" + msg } func (conn *Conn) Privmsg(t, msg string) { conn.out <- PRIVMSG + " " + t + " :" + msg }
// Notice() sends a NOTICE to the target t // Notice() sends a NOTICE to the target t
func (conn *Conn) Notice(t, msg string) { conn.out <- "NOTICE " + t + " :" + msg } func (conn *Conn) Notice(t, msg string) { conn.out <- NOTICE + " " + t + " :" + msg }
// Ctcp() sends a (generic) CTCP message to the target t // Ctcp() sends a (generic) CTCP message to the target t
// with an optional argument // with an optional argument
@ -87,10 +115,10 @@ func (conn *Conn) CtcpReply(t, ctcp string, arg ...string) {
} }
// Version() sends a CTCP "VERSION" to the target t // Version() sends a CTCP "VERSION" to the target t
func (conn *Conn) Version(t string) { conn.Ctcp(t, "VERSION") } func (conn *Conn) Version(t string) { conn.Ctcp(t, VERSION) }
// Action() sends a CTCP "ACTION" to the target t // Action() sends a CTCP "ACTION" to the target t
func (conn *Conn) Action(t, msg string) { conn.Ctcp(t, "ACTION", msg) } func (conn *Conn) Action(t, msg string) { conn.Ctcp(t, ACTION, msg) }
// Topic() sends a TOPIC command to the channel // Topic() sends a TOPIC command to the channel
// Topic(channel) retrieves the current channel topic (see "332" handler) // Topic(channel) retrieves the current channel topic (see "332" handler)
@ -100,7 +128,7 @@ func (conn *Conn) Topic(channel string, topic ...string) {
if t != "" { if t != "" {
t = " :" + t t = " :" + t
} }
conn.out <- "TOPIC " + channel + t conn.out <- TOPIC + " " + channel + t
} }
// Mode() sends a MODE command to the server. This one can get complicated if // Mode() sends a MODE command to the server. This one can get complicated if
@ -115,7 +143,7 @@ func (conn *Conn) Mode(t string, modestring ...string) {
if mode != "" { if mode != "" {
mode = " " + mode mode = " " + mode
} }
conn.out <- "MODE " + t + mode conn.out <- MODE + " " + t + mode
} }
// Away() sends an AWAY command to the server // Away() sends an AWAY command to the server
@ -126,15 +154,18 @@ func (conn *Conn) Away(message ...string) {
if msg != "" { if msg != "" {
msg = " :" + msg msg = " :" + msg
} }
conn.out <- "AWAY" + msg conn.out <- AWAY + msg
} }
// Invite() sends an INVITE command to the server // Invite() sends an INVITE command to the server
func (conn *Conn) Invite(nick, channel string) { func (conn *Conn) Invite(nick, channel string) { conn.out <- INVITE + " " + nick + " " + channel }
conn.out <- "INVITE " + nick + " " + channel
}
// Oper() sends an OPER command to the server // Oper() sends an OPER command to the server
func (conn *Conn) Oper(user, pass string) { func (conn *Conn) Oper(user, pass string) { conn.out <- OPER + " " + user + " " + pass }
conn.out <- "OPER " + user + " " + pass
} // Ping() sends a PING command to the server
// A PONG response is to be expected afterwards
func (conn *Conn) Ping(message string) { conn.out <- PING + " :" + message }
// Pong() sends a PONG command to the server
func (conn *Conn) Pong(message string) { conn.out <- PONG + " :" + message }

View File

@ -75,4 +75,10 @@ func TestClientCommands(t *testing.T) {
c.Oper("user", "pass") c.Oper("user", "pass")
s.nc.Expect("OPER user pass") s.nc.Expect("OPER user pass")
c.Ping("woot")
s.nc.Expect("PING :woot")
c.Pong("pwoot")
s.nc.Expect("PONG :pwoot")
} }

View File

@ -21,8 +21,8 @@ type Conn struct {
cfg *Config cfg *Config
// Handlers and Commands // Handlers and Commands
handlers *hSet handlers *handlerSet
commands *cSet commands *commandList
// State tracker for nicks and channels // State tracker for nicks and channels
st state.Tracker st state.Tracker
@ -62,7 +62,7 @@ type Config struct {
PingFreq time.Duration PingFreq time.Duration
// Controls what is stripped from line.Args[1] for Commands // Controls what is stripped from line.Args[1] for Commands
CommandStripNick, CommandStripPrefix bool CommandStripNick, SimpleCommandStripPrefix bool
// Set this to true to disable flood protection and false to re-enable // Set this to true to disable flood protection and false to re-enable
Flood bool Flood bool
@ -104,8 +104,8 @@ func Client(cfg *Config) (*Conn, error) {
cSend: make(chan bool), cSend: make(chan bool),
cLoop: make(chan bool), cLoop: make(chan bool),
cPing: make(chan bool), cPing: make(chan bool),
handlers: handlerSet(), handlers: newHandlerSet(),
commands: commandSet(), commands: newCommandList(),
stRemovers: make([]Remover, 0, len(stHandlers)), stRemovers: make([]Remover, 0, len(stHandlers)),
lastsent: time.Now(), lastsent: time.Now(),
} }

View File

@ -32,6 +32,7 @@ func setUp(t *testing.T, start ...bool) (*Conn, *testState) {
// Hack to allow tests of send, recv, write etc. // Hack to allow tests of send, recv, write etc.
// NOTE: the value of the boolean doesn't matter. // NOTE: the value of the boolean doesn't matter.
c.postConnect() c.postConnect()
// Sleep 1ms to allow background routines to start. // Sleep 1ms to allow background routines to start.
<-time.After(1e6) <-time.After(1e6)
} }
@ -56,7 +57,7 @@ func TestEOF(t *testing.T) {
// Set up a handler to detect whether disconnected handlers are called // Set up a handler to detect whether disconnected handlers are called
dcon := false dcon := false
c.HandleFunc("disconnected", func(conn *Conn, line *Line) { c.HandleFunc(DISCONNECTED, func(conn *Conn, line *Line) {
dcon = true dcon = true
}) })

View File

@ -1,7 +1,11 @@
package client package client
import ( import (
"container/list"
"fmt"
"github.com/fluffle/golog/logging" "github.com/fluffle/golog/logging"
"math"
"regexp"
"strings" "strings"
"sync" "sync"
) )
@ -11,189 +15,141 @@ type Handler interface {
Handle(*Conn, *Line) Handle(*Conn, *Line)
} }
// And when they've been added to the client they are removable.
type Remover interface {
Remove()
}
type HandlerFunc func(*Conn, *Line) type HandlerFunc func(*Conn, *Line)
func (hf HandlerFunc) Handle(conn *Conn, line *Line) { func (hf HandlerFunc) Handle(conn *Conn, line *Line) {
hf(conn, line) hf(conn, line)
} }
type hList struct { // And when they've been added to the client they are removable.
start, end *hNode type Remover interface {
Remove()
} }
type hNode struct { type RemoverFunc func()
next, prev *hNode
set *hSet func (r RemoverFunc) Remove() {
r()
}
type handlerElement struct {
event string event string
handler Handler handler Handler
} }
func (hn *hNode) Handle(conn *Conn, line *Line) { type handlerSet struct {
hn.handler.Handle(conn, line) set map[string]*list.List
}
func (hn *hNode) Remove() {
hn.set.remove(hn)
}
type hSet struct {
set map[string]*hList
sync.RWMutex sync.RWMutex
} }
func handlerSet() *hSet { func newHandlerSet() *handlerSet {
return &hSet{set: make(map[string]*hList)} return &handlerSet{set: make(map[string]*list.List)}
} }
func (hs *hSet) add(ev string, h Handler) Remover { func (hs *handlerSet) add(event string, handler Handler) (*list.Element, Remover) {
hs.Lock() hs.Lock()
defer hs.Unlock() defer hs.Unlock()
ev = strings.ToLower(ev) event = strings.ToLower(event)
l, ok := hs.set[ev] l, ok := hs.set[event]
if !ok { if !ok {
l = &hList{} l = list.New()
hs.set[event] = l
} }
hn := &hNode{ element := l.PushBack(&handlerElement{event, handler})
set: hs, return element, RemoverFunc(func() {
event: ev, hs.remove(element)
handler: h, })
}
if !ok {
l.start = hn
} else {
hn.prev = l.end
l.end.next = hn
}
l.end = hn
hs.set[ev] = l
return hn
} }
func (hs *hSet) remove(hn *hNode) { func (hs *handlerSet) remove(element *list.Element) {
hs.Lock() hs.Lock()
defer hs.Unlock() defer hs.Unlock()
l, ok := hs.set[hn.event] h := element.Value.(*handlerElement)
l, ok := hs.set[h.event]
if !ok { if !ok {
logging.Error("Removing node for unknown event '%s'", hn.event) logging.Error("Removing node for unknown event '%s'", h.event)
return return
} }
if hn.next == nil { l.Remove(element)
l.end = hn.prev if l.Len() == 0 {
} else { delete(hs.set, h.event)
hn.next.prev = hn.prev
}
if hn.prev == nil {
l.start = hn.next
} else {
hn.prev.next = hn.next
}
hn.next = nil
hn.prev = nil
hn.set = nil
if l.start == nil || l.end == nil {
delete(hs.set, hn.event)
} }
} }
func (hs *hSet) dispatch(conn *Conn, line *Line) { func (hs *handlerSet) dispatch(conn *Conn, line *Line) {
hs.RLock() hs.RLock()
defer hs.RUnlock() defer hs.RUnlock()
ev := strings.ToLower(line.Cmd) event := strings.ToLower(line.Cmd)
list, ok := hs.set[ev] l, ok := hs.set[event]
if !ok { if !ok {
return return
} }
for hn := list.start; hn != nil; hn = hn.next {
go hn.Handle(conn, line) for e := l.Front(); e != nil; e = e.Next() {
h := e.Value.(*handlerElement)
go h.handler.Handle(conn, line)
} }
} }
// An IRC command looks like this: type commandElement struct {
type Command interface { regex string
Execute(*Conn, *Line) handler Handler
Help() string priority int
} }
type command struct { type commandList struct {
fn HandlerFunc list *list.List
help string
}
func (c *command) Execute(conn *Conn, line *Line) {
c.fn(conn, line)
}
func (c *command) Help() string {
return c.help
}
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
sync.RWMutex sync.RWMutex
} }
func commandSet() *cSet { func newCommandList() *commandList {
return &cSet{set: make(map[string]*cNode)} return &commandList{list: list.New()}
} }
func (cs *cSet) add(pf string, c Command) Remover { func (cl *commandList) add(regex string, handler Handler, priority int) (element *list.Element, remover Remover) {
cs.Lock() cl.Lock()
defer cs.Unlock() defer cl.Unlock()
pf = strings.ToLower(pf) c := &commandElement{
if _, ok := cs.set[pf]; ok { regex: regex,
logging.Error("Command prefix '%s' already registered.", pf) handler: handler,
return nil priority: priority,
} }
cn := &cNode{ // Check for exact regex matches. This will filter out any repeated SimpleCommands.
cmd: c, for e := cl.list.Front(); e != nil; e = e.Next() {
set: cs, c := e.Value.(*commandElement)
prefix: pf, if c.regex == regex {
logging.Error("Command prefix '%s' already registered.", regex)
return
} }
cs.set[pf] = cn }
return cn element = cl.list.PushBack(c)
remover = RemoverFunc(func() {
cl.remove(element)
})
return
} }
func (cs *cSet) remove(cn *cNode) { func (cl *commandList) remove(element *list.Element) {
cs.Lock() cl.Lock()
defer cs.Unlock() defer cl.Unlock()
delete(cs.set, cn.prefix) cl.list.Remove(element)
cn.set = nil
} }
func (cs *cSet) match(txt string) (final Command, prefixlen int) { // Matches the command with the highest priority.
cs.RLock() func (cl *commandList) match(text string) (handler Handler) {
defer cs.RUnlock() cl.RLock()
txt = strings.ToLower(txt) defer cl.RUnlock()
for prefix, cmd := range cs.set { maxPriority := math.MinInt32
if !strings.HasPrefix(txt, prefix) { text = strings.ToLower(text)
continue 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
}
} }
if final == nil || len(prefix) > prefixlen {
prefixlen = len(prefix)
final = cmd
} }
} }
return return
@ -205,26 +161,75 @@ func (cs *cSet) match(txt string) (final Command, prefixlen int) {
// "PRIVMSG", "JOIN", etc. but all the numeric replies are left as ascii // "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 // strings of digits like "332" (mainly because I really didn't feel like
// putting massive constant tables in). // putting massive constant tables in).
func (conn *Conn) Handle(name string, h Handler) Remover { func (conn *Conn) Handle(name string, handler Handler) Remover {
return conn.handlers.add(name, h) _, remover := conn.handlers.add(name, handler)
return remover
} }
func (conn *Conn) HandleFunc(name string, hf HandlerFunc) Remover { func (conn *Conn) HandleFunc(name string, handlerFunc HandlerFunc) Remover {
return conn.Handle(name, hf) return conn.Handle(name, handlerFunc)
} }
func (conn *Conn) Command(prefix string, c Command) Remover { func (conn *Conn) Command(regex string, handler Handler, priority int) Remover {
return conn.commands.add(prefix, c) _, remover := conn.commands.add(regex, handler, priority)
return remover
} }
func (conn *Conn) CommandFunc(prefix string, hf HandlerFunc, help string) Remover { func (conn *Conn) CommandFunc(regex string, handlerFunc HandlerFunc, priority int) Remover {
return conn.Command(prefix, &command{hf, help}) 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.CommandFunc(fmt.Sprintf(SimpleCommandRegex, strings.ToLower(prefix)), 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) { func (conn *Conn) dispatch(line *Line) {
conn.handlers.dispatch(conn, line) conn.handlers.dispatch(conn, line)
} }
func (conn *Conn) cmdMatch(txt string) (Command, int) { func (conn *Conn) command(line *Line) {
return conn.commands.match(txt) command := conn.commands.match(line.Message())
if command != nil {
go command.Handle(conn, line)
}
} }

View File

@ -6,7 +6,7 @@ import (
) )
func TestHandlerSet(t *testing.T) { func TestHandlerSet(t *testing.T) {
hs := handlerSet() hs := newHandlerSet()
if len(hs.set) != 0 { if len(hs.set) != 0 {
t.Errorf("New set contains things!") t.Errorf("New set contains things!")
} }
@ -17,66 +17,40 @@ func TestHandlerSet(t *testing.T) {
} }
// Add one // Add one
hn1 := hs.add("ONE", HandlerFunc(f)).(*hNode) _, hn1 := hs.add("ONE", HandlerFunc(f))
hl, ok := hs.set["one"] hl, ok := hs.set["one"]
if len(hs.set) != 1 || !ok { if len(hs.set) != 1 || !ok {
t.Errorf("Set doesn't contain 'one' list after add().") t.Errorf("Set doesn't contain 'one' list after add().")
} }
if hn1.set != hs || hn1.event != "one" || hn1.prev != nil || hn1.next != nil { if hl.Len() != 1 {
t.Errorf("First node for 'one' not created correctly") t.Errorf("List doesn't contain 'one' after add().")
}
if hl.start != hn1 || hl.end != hn1 {
t.Errorf("Node not added to empty 'one' list correctly.")
} }
// Add another one... // Add another one...
hn2 := hs.add("one", HandlerFunc(f)).(*hNode) _, hn2 := hs.add("one", HandlerFunc(f))
if len(hs.set) != 1 { if len(hs.set) != 1 {
t.Errorf("Set contains more than 'one' list after add().") t.Errorf("Set contains more than 'one' list after add().")
} }
if hn2.set != hs || hn2.event != "one" { if hl.Len() != 2 {
t.Errorf("Second node for 'one' not created correctly") t.Errorf("List doesn't contain second 'one' after add().")
}
if hn1.prev != nil || hn1.next != hn2 || hn2.prev != hn1 || hn2.next != nil {
t.Errorf("Nodes for 'one' not linked correctly.")
}
if hl.start != hn1 || hl.end != hn2 {
t.Errorf("Node not appended to 'one' list correctly.")
} }
// Add a third one! // Add a third one!
hn3 := hs.add("one", HandlerFunc(f)).(*hNode) _, hn3 := hs.add("one", HandlerFunc(f))
if len(hs.set) != 1 { if len(hs.set) != 1 {
t.Errorf("Set contains more than 'one' list after add().") t.Errorf("Set contains more than 'one' list after add().")
} }
if hn3.set != hs || hn3.event != "one" { if hl.Len() != 3 {
t.Errorf("Third node for 'one' not created correctly") t.Errorf("List doesn't contain third 'one' after add().")
}
if hn1.prev != nil || hn1.next != hn2 ||
hn2.prev != hn1 || hn2.next != hn3 ||
hn3.prev != hn2 || hn3.next != nil {
t.Errorf("Nodes for 'one' not linked correctly.")
}
if hl.start != hn1 || hl.end != hn3 {
t.Errorf("Node not appended to 'one' list correctly.")
} }
// And finally a fourth one! // And finally a fourth one!
hn4 := hs.add("one", HandlerFunc(f)).(*hNode) _, hn4 := hs.add("one", HandlerFunc(f))
if len(hs.set) != 1 { if len(hs.set) != 1 {
t.Errorf("Set contains more than 'one' list after add().") t.Errorf("Set contains more than 'one' list after add().")
} }
if hn4.set != hs || hn4.event != "one" { if hl.Len() != 4 {
t.Errorf("Fourth node for 'one' not created correctly.") t.Errorf("List doesn't contain fourth 'one' after add().")
}
if hn1.prev != nil || hn1.next != hn2 ||
hn2.prev != hn1 || hn2.next != hn3 ||
hn3.prev != hn2 || hn3.next != hn4 ||
hn4.prev != hn3 || hn4.next != nil {
t.Errorf("Nodes for 'one' not linked correctly.")
}
if hl.start != hn1 || hl.end != hn4 {
t.Errorf("Node not appended to 'one' list correctly.")
} }
// Dispatch should result in 4 additions. // Dispatch should result in 4 additions.
@ -94,16 +68,8 @@ func TestHandlerSet(t *testing.T) {
if len(hs.set) != 1 { if len(hs.set) != 1 {
t.Errorf("Set list count changed after remove().") t.Errorf("Set list count changed after remove().")
} }
if hn3.set != nil || hn3.prev != nil || hn3.next != nil { if hl.Len() != 3 {
t.Errorf("Third node for 'one' not removed correctly.") t.Errorf("Third 'one' not removed correctly.")
}
if hn1.prev != nil || hn1.next != hn2 ||
hn2.prev != hn1 || hn2.next != hn4 ||
hn4.prev != hn2 || hn4.next != nil {
t.Errorf("Third node for 'one' not unlinked correctly.")
}
if hl.start != hn1 || hl.end != hn4 {
t.Errorf("Third node for 'one' changed list pointers.")
} }
// Dispatch should result in 3 additions. // Dispatch should result in 3 additions.
@ -114,18 +80,12 @@ func TestHandlerSet(t *testing.T) {
} }
// Remove node 1. // Remove node 1.
hs.remove(hn1) hn1.Remove()
if len(hs.set) != 1 { if len(hs.set) != 1 {
t.Errorf("Set list count changed after remove().") t.Errorf("Set list count changed after remove().")
} }
if hn1.set != nil || hn1.prev != nil || hn1.next != nil { if hl.Len() != 2 {
t.Errorf("First node for 'one' not removed correctly.") t.Errorf("First 'one' not removed correctly.")
}
if hn2.prev != nil || hn2.next != hn4 || hn4.prev != hn2 || hn4.next != nil {
t.Errorf("First node for 'one' not unlinked correctly.")
}
if hl.start != hn2 || hl.end != hn4 {
t.Errorf("First node for 'one' didn't change list pointers.")
} }
// Dispatch should result in 2 additions. // Dispatch should result in 2 additions.
@ -140,14 +100,8 @@ func TestHandlerSet(t *testing.T) {
if len(hs.set) != 1 { if len(hs.set) != 1 {
t.Errorf("Set list count changed after remove().") t.Errorf("Set list count changed after remove().")
} }
if hn4.set != nil || hn4.prev != nil || hn4.next != nil { if hl.Len() != 1 {
t.Errorf("Fourth node for 'one' not removed correctly.") t.Errorf("Fourth 'one' not removed correctly.")
}
if hn2.prev != nil || hn2.next != nil {
t.Errorf("Fourth node for 'one' not unlinked correctly.")
}
if hl.start != hn2 || hl.end != hn2 {
t.Errorf("Fourth node for 'one' didn't change list pointers.")
} }
// Dispatch should result in 1 addition. // Dispatch should result in 1 addition.
@ -158,16 +112,10 @@ func TestHandlerSet(t *testing.T) {
} }
// Remove node 2. // Remove node 2.
hs.remove(hn2) hn2.Remove()
if len(hs.set) != 0 { if len(hs.set) != 0 {
t.Errorf("Removing last node in 'one' didn't remove list.") t.Errorf("Removing last node in 'one' didn't remove list.")
} }
if hn2.set != nil || hn2.prev != nil || hn2.next != nil {
t.Errorf("Second node for 'one' not removed correctly.")
}
if hl.start != nil || hl.end != nil {
t.Errorf("Second node for 'one' didn't change list pointers.")
}
// Dispatch should result in NO additions. // Dispatch should result in NO additions.
hs.dispatch(nil, &Line{Cmd: "One"}) hs.dispatch(nil, &Line{Cmd: "One"})
@ -178,52 +126,43 @@ func TestHandlerSet(t *testing.T) {
} }
func TestCommandSet(t *testing.T) { func TestCommandSet(t *testing.T) {
cs := commandSet() cl := newCommandList()
if len(cs.set) != 0 { if cl.list.Len() != 0 {
t.Errorf("New set contains things!") t.Errorf("New list contains things!")
} }
c := &command{ _, cn1 := cl.add("one", HandlerFunc(func(c *Conn, l *Line) {}), 0)
fn: func(c *Conn, l *Line) {}, if cl.list.Len() != 1 {
help: "wtf?", t.Errorf("Command 'one' not added to list correctly.")
} }
cn1 := cs.add("ONE", c).(*cNode) _, cn2 := cl.add("one two", HandlerFunc(func(c *Conn, l *Line) {}), 0)
if _, ok := cs.set["one"]; !ok || cn1.set != cs || cn1.prefix != "one" { if cl.list.Len() != 2 {
t.Errorf("Command 'one' not added to set correctly.")
}
if fail := cs.add("one", c); fail != nil {
t.Errorf("Adding a second 'one' command did not fail as expected.")
}
cn2 := cs.add("One Two", c).(*cNode)
if _, ok := cs.set["one two"]; !ok || cn2.set != cs || cn2.prefix != "one two" {
t.Errorf("Command 'one two' not added to set correctly.") t.Errorf("Command 'one two' not added to set correctly.")
} }
if c, l := cs.match("foo"); c != nil || l != 0 { if c := cl.match("foo"); c != nil {
t.Errorf("Matched 'foo' when we shouldn't.") t.Errorf("Matched 'foo' when we shouldn't.")
} }
if c, l := cs.match("one"); c.(*cNode) != cn1 || l != 3 { if c := cl.match("one"); c == nil {
t.Errorf("Didn't match 'one' when we should have.") t.Errorf("Didn't match when we should have.")
} }
if c, l := cs.match("one two three"); c.(*cNode) != cn2 || l != 7 { if c := cl.match("one two three"); c == nil {
t.Errorf("Didn't match 'one two' when we should have.") t.Errorf("Didn't match when we should have.")
} }
cs.remove(cn2) cn2.Remove()
if _, ok := cs.set["one two"]; ok || cn2.set != nil { if cl.list.Len() != 1 {
t.Errorf("Command 'one two' not removed correctly.") t.Errorf("Command 'one two' not removed correctly.")
} }
if c, l := cs.match("one two three"); c.(*cNode) != cn1 || l != 3 { if c := cl.match("one two three"); c == nil {
t.Errorf("Didn't match 'one' when we should have.") t.Errorf("Didn't match when we should have.")
} }
cn1.Remove() cn1.Remove()
if _, ok := cs.set["one"]; ok || cn1.set != nil { if cl.list.Len() != 0 {
t.Errorf("Command 'one' not removed correctly.") t.Errorf("Command 'one two' not removed correctly.")
} }
if c, l := cs.match("one two three"); c != nil || l != 0 { if c := cl.match("one two three"); c != nil {
t.Errorf("Matched 'one' when we shouldn't have.") t.Errorf("Matched 'one' when we shouldn't.")
} }
} }

75
client/funcs.go Normal file
View File

@ -0,0 +1,75 @@
package client
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"regexp"
"strings"
)
type youTubeVideo struct {
Entry struct {
Info struct {
Title struct {
Text string `json:"$t"`
} `json:"media$title"`
Description struct {
Text string `json:"$t"`
} `json:"media$description"`
} `json:"media$group"`
Rating struct {
Likes string `json:"numLikes"`
Dislikes string `json:"numDislikes"`
} `json:"yt$rating"`
Statistics struct {
Views string `json:"viewCount"`
} `json:"yt$statistics"`
} `json:entry`
}
const UrlRegex string = `(\s|^)(http://|https://)(.*?)(\s|$)`
func UrlFunc(conn *Conn, line *Line) {
text := line.Message()
if regex, err := regexp.Compile(UrlRegex); err == nil {
url := strings.TrimSpace(regex.FindString(text))
if url != "" {
if resp, err := http.Get(url); err == nil {
if strings.HasPrefix(resp.Header.Get("Content-Type"), "text/html") {
defer resp.Body.Close()
if content, err := ioutil.ReadAll(resp.Body); err == nil {
if regex, err := regexp.Compile(`<title>(.*?)</title>`); err == nil {
if regex.Match([]byte(content)) {
conn.Privmsg(line.Target(), strings.TrimSpace(regex.FindStringSubmatch(string(content))[1]))
}
}
}
}
}
}
}
}
const 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(YouTubeRegex); err == nil {
if regex.Match([]byte(text)) {
matches := regex.FindStringSubmatch(text)
id := matches[len(matches)-2]
url := fmt.Sprintf("https://gdata.youtube.com/feeds/api/videos/%s?v=2&alt=json", id)
if resp, err := http.Get(url); err == nil {
defer resp.Body.Close()
if contents, err := ioutil.ReadAll(resp.Body); err == nil {
var data youTubeVideo
if err := json.Unmarshal(contents, &data); err == nil {
conn.Privmsg(line.Target(), fmt.Sprintf("%s - %s views (%s likes, %s dislikes)", data.Entry.Info.Title.Text, data.Entry.Statistics.Views, data.Entry.Rating.Likes, data.Entry.Rating.Dislikes))
}
}
}
}
}
}

View File

@ -7,20 +7,15 @@ import (
"strings" "strings"
) )
const (
REGISTER = "REGISTER"
CONNECTED = "CONNECTED"
DISCONNECTED = "DISCONNECTED"
)
// sets up the internal event handlers to do essential IRC protocol things // sets up the internal event handlers to do essential IRC protocol things
var intHandlers = map[string]HandlerFunc{ var intHandlers = map[string]HandlerFunc{
REGISTER: (*Conn).h_REGISTER, REGISTER: (*Conn).h_REGISTER,
"001": (*Conn).h_001, "001": (*Conn).h_001,
"433": (*Conn).h_433, "433": (*Conn).h_433,
"CTCP": (*Conn).h_CTCP, CTCP: (*Conn).h_CTCP,
"NICK": (*Conn).h_NICK, NICK: (*Conn).h_NICK,
"PING": (*Conn).h_PING, PING: (*Conn).h_PING,
PRIVMSG: (*Conn).h_PRIVMSG,
} }
func (conn *Conn) addIntHandlers() { func (conn *Conn) addIntHandlers() {
@ -33,7 +28,7 @@ func (conn *Conn) addIntHandlers() {
// Basic ping/pong handler // Basic ping/pong handler
func (conn *Conn) h_PING(line *Line) { func (conn *Conn) h_PING(line *Line) {
conn.Raw("PONG :" + line.Args[0]) conn.Pong(line.Args[0])
} }
// Handler for initial registration with server once tcp connection is made. // Handler for initial registration with server once tcp connection is made.
@ -86,10 +81,10 @@ func (conn *Conn) h_433(line *Line) {
// Handle VERSION requests and CTCP PING // Handle VERSION requests and CTCP PING
func (conn *Conn) h_CTCP(line *Line) { func (conn *Conn) h_CTCP(line *Line) {
if line.Args[0] == "VERSION" { if line.Args[0] == VERSION {
conn.CtcpReply(line.Nick, "VERSION", "powered by goirc...") conn.CtcpReply(line.Nick, VERSION, "powered by goirc...")
} else if line.Args[0] == "PING" { } else if line.Args[0] == PING {
conn.CtcpReply(line.Nick, "PING", line.Args[2]) conn.CtcpReply(line.Nick, PING, line.Args[2])
} }
} }
@ -102,34 +97,19 @@ func (conn *Conn) h_NICK(line *Line) {
// Handle PRIVMSGs that trigger Commands // Handle PRIVMSGs that trigger Commands
func (conn *Conn) h_PRIVMSG(line *Line) { func (conn *Conn) h_PRIVMSG(line *Line) {
txt := line.Args[1] text := line.Message()
if conn.cfg.CommandStripNick && strings.HasPrefix(txt, conn.cfg.Me.Nick) { if conn.cfg.CommandStripNick && strings.HasPrefix(text, conn.cfg.Me.Nick) {
// Look for '^${nick}[:;>,-]? ' // Look for '^${nick}[:;>,-]? '
l := len(conn.cfg.Me.Nick) l := len(conn.cfg.Me.Nick)
switch txt[l] { switch text[l] {
case ':', ';', '>', ',', '-': case ':', ';', '>', ',', '-':
l++ l++
} }
if txt[l] == ' ' { if text[l] == ' ' {
txt = strings.TrimSpace(txt[l:]) text = strings.TrimSpace(text[l:])
} }
}
cmd, l := conn.cmdMatch(txt)
if cmd == nil {
return
}
if conn.cfg.CommandStripPrefix {
txt = strings.TrimSpace(txt[l:])
}
if txt != line.Args[1] {
line = line.Copy() line = line.Copy()
line.Args[1] = txt line.Args[1] = text
}
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)
} }

View File

@ -47,7 +47,7 @@ func Test001(t *testing.T) {
l := parseLine(":irc.server.org 001 test :Welcome to IRC test!ident@somehost.com") l := parseLine(":irc.server.org 001 test :Welcome to IRC test!ident@somehost.com")
// Set up a handler to detect whether connected handler is called from 001 // Set up a handler to detect whether connected handler is called from 001
hcon := false hcon := false
c.HandleFunc("connected", func(conn *Conn, line *Line) { c.HandleFunc(CONNECTED, func(conn *Conn, line *Line) {
hcon = true hcon = true
}) })
@ -166,7 +166,9 @@ func TestPRIVMSG(t *testing.T) {
f := func(conn *Conn, line *Line) { f := func(conn *Conn, line *Line) {
conn.Privmsg(line.Args[0], line.Args[1]) conn.Privmsg(line.Args[0], line.Args[1])
} }
c.CommandFunc("prefix", f, "") // Test legacy simpleCommands, with out the !.
SimpleCommandRegex = `^%v(\s|$)`
c.SimpleCommandFunc("prefix", f)
// CommandStripNick and CommandStripPrefix are both false to begin // CommandStripNick and CommandStripPrefix are both false to begin
c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :prefix bar")) c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :prefix bar"))
@ -183,7 +185,7 @@ func TestPRIVMSG(t *testing.T) {
c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :test: prefix bar")) c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :test: prefix bar"))
s.nc.Expect("PRIVMSG #foo :prefix bar") s.nc.Expect("PRIVMSG #foo :prefix bar")
c.cfg.CommandStripPrefix = true c.cfg.SimpleCommandStripPrefix = true
c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :prefix bar")) c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :prefix bar"))
s.nc.Expect("PRIVMSG #foo :bar") s.nc.Expect("PRIVMSG #foo :bar")
c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :test: prefix bar")) c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :test: prefix bar"))

View File

@ -62,7 +62,7 @@ func parseLine(s string) *Line {
// So, I think CTCP and (in particular) CTCP ACTION are better handled as // So, I think CTCP and (in particular) CTCP ACTION are better handled as
// separate events as opposed to forcing people to have gargantuan // separate events as opposed to forcing people to have gargantuan
// handlers to cope with the possibilities. // handlers to cope with the possibilities.
if (line.Cmd == "PRIVMSG" || line.Cmd == "NOTICE") && if (line.Cmd == PRIVMSG || line.Cmd == NOTICE) &&
len(line.Args[1]) > 2 && len(line.Args[1]) > 2 &&
strings.HasPrefix(line.Args[1], "\001") && strings.HasPrefix(line.Args[1], "\001") &&
strings.HasSuffix(line.Args[1], "\001") { strings.HasSuffix(line.Args[1], "\001") {
@ -72,19 +72,42 @@ func parseLine(s string) *Line {
// Replace the line with the unwrapped CTCP // Replace the line with the unwrapped CTCP
line.Args[1] = t[1] line.Args[1] = t[1]
} }
if c := strings.ToUpper(t[0]); c == "ACTION" && line.Cmd == "PRIVMSG" { if c := strings.ToUpper(t[0]); c == ACTION && line.Cmd == PRIVMSG {
// make a CTCP ACTION it's own event a-la PRIVMSG // make a CTCP ACTION it's own event a-la PRIVMSG
line.Cmd = c line.Cmd = c
} else { } else {
// otherwise, dispatch a generic CTCP/CTCPREPLY event that // otherwise, dispatch a generic CTCP/CTCPREPLY event that
// contains the type of CTCP in line.Args[0] // contains the type of CTCP in line.Args[0]
if line.Cmd == "PRIVMSG" { if line.Cmd == PRIVMSG {
line.Cmd = "CTCP" line.Cmd = CTCP
} else { } else {
line.Cmd = "CTCPREPLY" line.Cmd = CTCPREPLY
} }
line.Args = append([]string{c}, line.Args...) line.Args = append([]string{c}, line.Args...)
} }
} }
return line return line
} }
// Return the contents of the message portion of a line.
// This only really makes sense for messages with a :message portion, but there
// are a lot of them.
func (line *Line) Message() string {
if len(line.Args) > 0 {
return line.Args[len(line.Args)-1]
}
return ""
}
// Return the target of the line. This only really makes sense for PRIVMSG.
// If the line was broadcast from a channel, the target will be that channel.
// If the line was broadcast by a user, the target will be that user.
func (line *Line) Target() string {
if line.Cmd == PRIVMSG {
if !strings.HasPrefix(line.Args[0], "#") {
return line.Nick
}
return line.Args[0]
}
return ""
}