Re-work Handlers for IRC events; add Commands.

This commit is contained in:
Alex Bramley 2013-02-16 00:17:31 +00:00
parent a038856094
commit a674267128
8 changed files with 692 additions and 158 deletions

View File

@ -5,7 +5,6 @@ import (
"crypto/tls"
"errors"
"fmt"
"github.com/fluffle/goevent/event"
"github.com/fluffle/goirc/state"
"github.com/fluffle/golog/logging"
"net"
@ -20,16 +19,14 @@ type Conn struct {
Me *state.Nick
Network string
// Replaceable function to customise the 433 handler's new nick
NewNick func(string) string
// Event handler registry and dispatcher
ER event.EventRegistry
ED event.EventDispatcher
// Handlers and Commands
handlers *hSet
commands *cSet
// State tracker for nicks and channels
ST state.StateTracker
st bool
ST state.StateTracker
st bool
stRemovers []Remover
// Use the State field to store external state that handlers might need.
// Remember ... you might need locking for this ;-)
@ -50,9 +47,15 @@ type Conn struct {
SSL bool
SSLConfig *tls.Config
// Replaceable function to customise the 433 handler's new nick
NewNick func(string) string
// 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
@ -62,9 +65,9 @@ type Conn struct {
}
// Creates a new IRC connection object, but doesn't connect to anything so
// that you can add event handlers to it. See AddHandler() for details.
func SimpleClient(nick string, args ...string) *Conn {
r := event.NewRegistry()
// that you can add event handlers to it. See AddHandler() for details
func Client(nick string, args ...string) *Conn {
logging.InitFromFlags()
ident := "goirc"
name := "Powered by GoIRC"
@ -74,30 +77,18 @@ func SimpleClient(nick string, args ...string) *Conn {
if len(args) > 1 && args[1] != "" {
name = args[1]
}
return Client(nick, ident, name, r)
}
func Client(nick, ident, name string, r event.EventRegistry) *Conn {
if r == nil {
return nil
}
logging.InitFromFlags()
conn := &Conn{
ER: r,
ED: r,
st: false,
in: make(chan *Line, 32),
out: make(chan string, 32),
cSend: make(chan bool),
cLoop: make(chan bool),
cPing: make(chan bool),
SSL: false,
SSLConfig: nil,
PingFreq: 3 * time.Minute,
Flood: false,
NewNick: func(s string) string { return s + "_" },
badness: 0,
lastsent: time.Now(),
in: make(chan *Line, 32),
out: make(chan string, 32),
cSend: make(chan bool),
cLoop: make(chan bool),
cPing: make(chan bool),
handlers: handlerSet(),
commands: commandSet(),
stRemovers: make([]Remover, 0, len(stHandlers)),
PingFreq: 3 * time.Minute,
NewNick: func(s string) string { return s + "_" },
lastsent: time.Now(),
}
conn.addIntHandlers()
conn.Me = state.NewNick(nick)
@ -257,7 +248,7 @@ func (conn *Conn) runLoop() {
for {
select {
case line := <-conn.in:
conn.ED.Dispatch(line.Cmd, conn, line)
conn.dispatch(line)
case <-conn.cLoop:
// strobe on control channel, bail out
return
@ -314,7 +305,7 @@ func (conn *Conn) shutdown() {
// as calling sock.Close() will cause recv() to recieve EOF in readstring()
if conn.Connected {
logging.Info("irc.shutdown(): Disconnected from server.")
conn.ED.Dispatch("disconnected", conn, &Line{})
conn.dispatch(&Line{Cmd: "disconnected"})
conn.Connected = false
conn.sock.Close()
conn.cSend <- true

View File

@ -3,7 +3,6 @@ package client
import (
"bufio"
"code.google.com/p/gomock/gomock"
"github.com/fluffle/goevent/event"
"github.com/fluffle/golog/logging"
"github.com/fluffle/goirc/state"
"strings"
@ -14,7 +13,6 @@ import (
type testState struct {
ctrl *gomock.Controller
st *state.MockStateTracker
ed *event.MockEventDispatcher
nc *mockNetConn
c *Conn
}
@ -22,13 +20,10 @@ type testState struct {
func setUp(t *testing.T, start ...bool) (*Conn, *testState) {
ctrl := gomock.NewController(t)
st := state.NewMockStateTracker(ctrl)
r := event.NewRegistry()
ed := event.NewMockEventDispatcher(ctrl)
nc := MockNetConn(t)
c := Client("test", "test", "Testing IRC", r)
c := Client("test", "test", "Testing IRC")
logging.SetLogLevel(logging.LogFatal)
c.ED = ed
c.ST = st
c.st = true
c.sock = nc
@ -42,15 +37,14 @@ func setUp(t *testing.T, start ...bool) (*Conn, *testState) {
<-time.After(1e6)
}
return c, &testState{ctrl, st, ed, nc, c}
return c, &testState{ctrl, st, nc, c}
}
func (s *testState) tearDown() {
s.ed.EXPECT().Dispatch("disconnected", s.c, &Line{})
s.st.EXPECT().Wipe()
s.nc.ExpectNothing()
s.c.shutdown()
<-time.After(1e6)
<-time.After(time.Millisecond)
s.ctrl.Finish()
}
@ -61,56 +55,61 @@ func TestEOF(t *testing.T) {
// Since we're not using tearDown() here, manually call Finish()
defer s.ctrl.Finish()
// Set up a handler to detect whether disconnected handlers are called
dcon := false
c.HandleFunc("disconnected", func (conn *Conn, line *Line) {
dcon = true
})
// Simulate EOF from server
s.ed.EXPECT().Dispatch("disconnected", c, &Line{})
s.st.EXPECT().Wipe()
s.nc.Close()
// Since things happen in different internal goroutines, we need to wait
// 1 ms should be enough :-)
<-time.After(1e6)
<-time.After(time.Millisecond)
// Verify that the connection no longer thinks it's connected
if c.Connected {
t.Errorf("Conn still thinks it's connected to the server.")
}
// Verify that disconnected handler was called
if !dcon {
t.Errorf("Conn did not call disconnected handlers.")
}
}
func TestClientAndStateTracking(t *testing.T) {
// This doesn't use setUp() as we want to pass in a mock EventRegistry.
ctrl := gomock.NewController(t)
r := event.NewMockEventRegistry(ctrl)
st := state.NewMockStateTracker(ctrl)
for n, _ := range intHandlers {
// We can't use EXPECT() here as comparisons of functions are
// no longer valid in Go, which causes reflect.DeepEqual to bail.
// Instead, ignore the function arg and just ensure that all the
// handler names are correctly passed to AddHandler.
ctrl.RecordCall(r, "AddHandler", gomock.Any(), n)
}
c := Client("test", "test", "Testing IRC", r)
c := Client("test", "test", "Testing IRC")
// Assert some basic things about the initial state of the Conn struct
if c.ER != r || c.ED != r || c.st != false || c.ST != nil {
t.Errorf("Conn not correctly initialised with external deps.")
}
if c.in == nil || c.out == nil || c.cSend == nil || c.cLoop == nil {
t.Errorf("Conn control channels not correctly initialised.")
}
if c.Me.Nick != "test" || c.Me.Ident != "test" ||
c.Me.Name != "Testing IRC" || c.Me.Host != "" {
t.Errorf("Conn.Me not correctly initialised.")
}
// OK, while we're here with a mock event registry...
for n, _ := range stHandlers {
// See above.
ctrl.RecordCall(r, "AddHandler", gomock.Any(), n)
// Check that the internal handlers are correctly set up
for k, _ := range intHandlers {
if _, ok := c.handlers.set[strings.ToLower(k)]; !ok {
t.Errorf("Missing internal handler for '%s'.", k)
}
}
c.EnableStateTracking()
// We're expecting the untracked me to be replaced by a tracked one.
// Now enable the state tracking code and check its handlers
c.EnableStateTracking()
for k, _ := range stHandlers {
if _, ok := c.handlers.set[strings.ToLower(k)]; !ok {
t.Errorf("Missing state handler for '%s'.", k)
}
}
if len(c.stRemovers) != len(stHandlers) {
t.Errorf("Incorrect number of Removers (%d != %d) when adding state handlers.",
len(c.stRemovers), len(stHandlers))
}
// We're expecting the untracked me to be replaced by a tracked one
if c.Me.Nick != "test" || c.Me.Ident != "test" ||
c.Me.Name != "Testing IRC" || c.Me.Host != "" {
t.Errorf("Enabling state tracking did not replace Me correctly.")
@ -119,18 +118,25 @@ func TestClientAndStateTracking(t *testing.T) {
t.Errorf("State tracker not enabled correctly.")
}
// Now, shim in the mock state tracker and test disabling state tracking.
// Now, shim in the mock state tracker and test disabling state tracking
me := c.Me
c.ST = st
st.EXPECT().Wipe()
for n, _ := range stHandlers {
// See above.
ctrl.RecordCall(r, "DelHandler", gomock.Any(), n)
}
c.DisableStateTracking()
if c.st || c.ST != nil || c.Me != me {
t.Errorf("State tracker not disabled correctly.")
}
// Finally, check state tracking handlers were all removed correctly
for k, _ := range stHandlers {
if _, ok := c.handlers.set[strings.ToLower(k)]; ok && k != "NICK" {
// A bit leaky, because intHandlers adds a NICK handler.
t.Errorf("State handler for '%s' not removed correctly.", k)
}
}
if len(c.stRemovers) != 0 {
t.Errorf("stRemovers not zeroed correctly when removing state handlers.")
}
ctrl.Finish()
}
@ -202,7 +208,7 @@ func TestRecv(t *testing.T) {
// reader is a helper to do a "non-blocking" read of c.in
reader := func() *Line {
select {
case <-time.After(1e6):
case <-time.After(time.Millisecond):
case l := <-c.in:
return l
}
@ -221,7 +227,7 @@ func TestRecv(t *testing.T) {
// Strangely, recv() needs some time to start up, but *only* when this test
// is run standalone with: client/_test/_testmain --test.run TestRecv
<-time.After(1e6)
<-time.After(time.Millisecond)
// Now, this should mean that we'll receive our parsed line on c.in
if l := reader(); l == nil || l.Cmd != "001" {
@ -245,7 +251,6 @@ func TestRecv(t *testing.T) {
if exited {
t.Errorf("Exited before socket close.")
}
s.ed.EXPECT().Dispatch("disconnected", c, &Line{})
s.st.EXPECT().Wipe()
s.nc.Close()
@ -255,7 +260,7 @@ func TestRecv(t *testing.T) {
<-c.cLoop
<-c.cPing
// Give things time to shake themselves out...
<-time.After(1e6)
<-time.After(time.Millisecond)
if !exited {
t.Errorf("Didn't exit on socket close.")
}
@ -296,7 +301,7 @@ func TestPing(t *testing.T) {
exited = true
}()
// The first ping should be after a second,
// The first ping should be after 50ms,
// so we don't expect anything now on c.in
if s := reader(); s != "" {
t.Errorf("Line output directly after ping started.")
@ -349,15 +354,25 @@ func TestRunLoop(t *testing.T) {
bufio.NewReader(c.sock),
bufio.NewWriter(c.sock))
// NOTE: here we assert that no Dispatch event has been called yet by
// calling s.ctrl.Finish(). There doesn't appear to be any harm in this.
// Set up a handler to detect whether 001 handler is called
h001 := false
c.HandleFunc("001", func (conn *Conn, line *Line) {
h001 = true
})
// Set up a handler to detect whether 002 handler is called
h002 := false
c.HandleFunc("002", func (conn *Conn, line *Line) {
h002 = true
})
l1 := parseLine(":irc.server.org 001 test :First test line.")
c.in <- l1
s.ctrl.Finish()
if h001 {
t.Errorf("001 handler called before runLoop started.")
}
// We want to test that the a goroutine calling runLoop will exit correctly.
// Now, we can expect the call to Dispatch to take place as runLoop starts.
s.ed.EXPECT().Dispatch("001", c, l1)
exited := false
go func() {
c.runLoop()
@ -365,15 +380,20 @@ func TestRunLoop(t *testing.T) {
}()
// Here, the opposite seemed to take place, with TestRunLoop failing when
// run as part of the suite but passing when run on it's own.
<-time.After(1e6)
<-time.After(time.Millisecond)
if !h001 {
t.Errorf("001 handler not called after runLoop started.")
}
// Send another line, just to be sure :-)
l2 := parseLine(":irc.server.org 002 test :Second test line.")
s.ed.EXPECT().Dispatch("002", c, l2)
c.in <- l2
// It appears some sleeping is needed after all of these to ensure channel
// sends occur before the close signal is sent below...
<-time.After(1e6)
<-time.After(time.Millisecond)
if !h002 {
t.Errorf("002 handler not called while runLoop started.")
}
// Now, use the control channel to exit send and kill the goroutine.
if exited {
@ -381,13 +401,17 @@ func TestRunLoop(t *testing.T) {
}
c.cLoop <- true
// Allow propagation time...
<-time.After(1e6)
<-time.After(time.Millisecond)
if !exited {
t.Errorf("Didn't exit after signal.")
}
// Sending more on c.in shouldn't dispatch any further events
h001 = false
c.in <- l1
if h001 {
t.Errorf("001 handler called after runLoop ended.")
}
}
func TestWrite(t *testing.T) {
@ -432,8 +456,6 @@ func TestWrite(t *testing.T) {
<-c.cPing
}()
s.nc.Close()
s.ed.EXPECT().Dispatch("disconnected", c, &Line{})
s.st.EXPECT().Wipe()
c.write("she can't pass unit tests")
}
@ -468,13 +490,14 @@ func TestRateLimit(t *testing.T) {
// characters as the line length means we should be increasing badness by
// 2.5 seconds minus the delta between the two ratelimit calls. This should
// be minimal but it's guaranteed that it won't be zero. Use 10us as a fuzz.
if l := c.rateLimit(60); l != 0 || abs(c.badness - 25*1e8) > 10 * time.Microsecond {
if l := c.rateLimit(60); l != 0 ||
abs(c.badness - 2500*time.Millisecond) > 10 * time.Microsecond {
t.Errorf("Rate limit calculating badness incorrectly.")
}
// At this point, we can tip over the badness scale, with a bit of help.
// 720 chars => +8 seconds of badness => 10.5 seconds => ratelimit
if l := c.rateLimit(720); l != 8 * time.Second ||
abs(c.badness - 105*1e8) > 10 * time.Microsecond {
abs(c.badness - 10500*time.Millisecond) > 10 * time.Microsecond {
t.Errorf("Rate limit failed to return correct limiting values.")
t.Errorf("l=%d, badness=%d", l, c.badness)
}

228
client/dispatch.go Normal file
View File

@ -0,0 +1,228 @@
package client
import (
"github.com/fluffle/golog/logging"
"strings"
"sync"
)
// An IRC handler looks like this:
type Handler interface {
Handle(*Conn, *Line)
}
// And when they've been added to the client they are removable.
type Remover interface {
Remove()
}
type HandlerFunc func(*Conn, *Line)
func (hf HandlerFunc) Handle(conn *Conn, line *Line) {
hf(conn, line)
}
type hList struct {
start, end *hNode
}
type hNode struct {
next, prev *hNode
set *hSet
event string
handler Handler
}
func (hn *hNode) Handle(conn *Conn, line *Line) {
hn.handler.Handle(conn, line)
}
func (hn *hNode) Remove() {
hn.set.remove(hn)
}
type hSet struct {
set map[string]*hList
sync.RWMutex
}
func handlerSet() *hSet {
return &hSet{set: make(map[string]*hList)}
}
func (hs *hSet) add(ev string, h Handler) Remover {
hs.Lock()
defer hs.Unlock()
ev = strings.ToLower(ev)
l, ok := hs.set[ev]
if !ok {
l = &hList{}
}
hn := &hNode{
set: hs,
event: ev,
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) {
hs.Lock()
defer hs.Unlock()
l, ok := hs.set[hn.event]
if !ok {
logging.Error("Removing node for unknown event '%s'", hn.event)
return
}
if hn.next == nil {
l.end = hn.prev
} else {
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) {
hs.RLock()
defer hs.RUnlock()
ev := strings.ToLower(line.Cmd)
list, ok := hs.set[ev]
if !ok { return }
for hn := list.start; hn != nil; hn = hn.next {
go hn.Handle(conn, line)
}
}
// An IRC command looks like this:
type Command interface {
Execute(*Conn, *Line)
Help() string
}
type command struct {
fn HandlerFunc
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
}
func commandSet() *cSet {
return &cSet{set: make(map[string]*cNode)}
}
func (cs *cSet) add(pf string, c Command) 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
}
cn := &cNode{
cmd: c,
set: cs,
prefix: pf,
}
cs.set[pf] = cn
return cn
}
func (cs *cSet) remove(cn *cNode) {
cs.Lock()
defer cs.Unlock()
delete(cs.set, cn.prefix)
cn.set = nil
}
func (cs *cSet) match(txt string) (final Command, prefixlen int) {
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
}
}
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, h Handler) Remover {
return conn.handlers.add(name, h)
}
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) CommandFunc(prefix string, hf HandlerFunc, help string) Remover {
return conn.Command(prefix, &command{hf, help})
}
func (conn *Conn) dispatch(line *Line) {
conn.handlers.dispatch(conn, line)
}
func (conn *Conn) cmdMatch(txt string) (Command, int) {
return conn.commands.match(txt)
}

229
client/dispatch_test.go Normal file
View File

@ -0,0 +1,229 @@
package client
import (
"testing"
"time"
)
func TestHandlerSet(t *testing.T) {
hs := handlerSet()
if len(hs.set) != 0 {
t.Errorf("New set contains things!")
}
callcount := 0
f := func(c *Conn, l *Line) {
callcount++
}
// Add one
hn1 := hs.add("ONE", HandlerFunc(f)).(*hNode)
hl, ok := hs.set["one"]
if len(hs.set) != 1 || !ok {
t.Errorf("Set doesn't contain 'one' list after add().")
}
if hn1.set != hs || hn1.event != "one" || hn1.prev != nil || hn1.next != nil {
t.Errorf("First node for 'one' not created correctly")
}
if hl.start != hn1 || hl.end != hn1 {
t.Errorf("Node not added to empty 'one' list correctly.")
}
// Add another one...
hn2 := hs.add("one", HandlerFunc(f)).(*hNode)
if len(hs.set) != 1 {
t.Errorf("Set contains more than 'one' list after add().")
}
if hn2.set != hs || hn2.event != "one" {
t.Errorf("Second node for 'one' not created correctly")
}
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!
hn3 := hs.add("one", HandlerFunc(f)).(*hNode)
if len(hs.set) != 1 {
t.Errorf("Set contains more than 'one' list after add().")
}
if hn3.set != hs || hn3.event != "one" {
t.Errorf("Third node for 'one' not created correctly")
}
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!
hn4 := hs.add("one", HandlerFunc(f)).(*hNode)
if len(hs.set) != 1 {
t.Errorf("Set contains more than 'one' list after add().")
}
if hn4.set != hs || hn4.event != "one" {
t.Errorf("Fourth node for 'one' not created correctly.")
}
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.
if callcount != 0 {
t.Errorf("Something incremented call count before we were expecting it.")
}
hs.dispatch(nil, &Line{Cmd:"One"})
<-time.After(time.Millisecond)
if callcount != 4 {
t.Errorf("Our handler wasn't called four times :-(")
}
// Remove node 3.
hn3.Remove()
if len(hs.set) != 1 {
t.Errorf("Set list count changed after remove().")
}
if hn3.set != nil || hn3.prev != nil || hn3.next != nil {
t.Errorf("Third node for '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.
hs.dispatch(nil, &Line{Cmd:"One"})
<-time.After(time.Millisecond)
if callcount != 7 {
t.Errorf("Our handler wasn't called three times :-(")
}
// Remove node 1.
hs.remove(hn1)
if len(hs.set) != 1 {
t.Errorf("Set list count changed after remove().")
}
if hn1.set != nil || hn1.prev != nil || hn1.next != nil {
t.Errorf("First node for '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.
hs.dispatch(nil, &Line{Cmd:"One"})
<-time.After(time.Millisecond)
if callcount != 9 {
t.Errorf("Our handler wasn't called two times :-(")
}
// Remove node 4.
hn4.Remove()
if len(hs.set) != 1 {
t.Errorf("Set list count changed after remove().")
}
if hn4.set != nil || hn4.prev != nil || hn4.next != nil {
t.Errorf("Fourth node for '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.
hs.dispatch(nil, &Line{Cmd:"One"})
<-time.After(time.Millisecond)
if callcount != 10 {
t.Errorf("Our handler wasn't called once :-(")
}
// Remove node 2.
hs.remove(hn2)
if len(hs.set) != 0 {
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.
hs.dispatch(nil, &Line{Cmd:"One"})
<-time.After(time.Millisecond)
if callcount != 10 {
t.Errorf("Our handler was called?")
}
}
func TestCommandSet(t *testing.T) {
cs := commandSet()
if len(cs.set) != 0 {
t.Errorf("New set contains things!")
}
c := &command{
fn: func(c *Conn, l *Line) {},
help: "wtf?",
}
cn1 := cs.add("ONE", c).(*cNode)
if _, ok := cs.set["one"]; !ok || cn1.set != cs || cn1.prefix != "one" {
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.")
}
if c, l := cs.match("foo"); c != nil || l != 0 {
t.Errorf("Matched 'foo' when we shouldn't.")
}
if c, l := cs.match("one"); c.(*cNode) != cn1 || l != 3 {
t.Errorf("Didn't match 'one' when we should have.")
}
if c, l := cs.match ("one two three"); c.(*cNode) != cn2 || l != 7 {
t.Errorf("Didn't match 'one two' when we should have.")
}
cs.remove(cn2)
if _, ok := cs.set["one two"]; ok || cn2.set != nil {
t.Errorf("Command 'one two' not removed correctly.")
}
if c, l := cs.match ("one two three"); c.(*cNode) != cn1 || l != 3 {
t.Errorf("Didn't match 'one' when we should have.")
}
cn1.Remove()
if _, ok := cs.set["one"]; ok || cn1.set != nil {
t.Errorf("Command 'one' not removed correctly.")
}
if c, l := cs.match ("one two three"); c != nil || l != 0 {
t.Errorf("Matched 'one' when we shouldn't have.")
}
}

View File

@ -4,48 +4,23 @@ package client
// to manage tracking an irc connection etc.
import (
"github.com/fluffle/goevent/event"
"strings"
)
// An IRC handler looks like this:
type IRCHandler func(*Conn, *Line)
// AddHandler() adds an event handler for a specific IRC command.
//
// 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) AddHandler(name string, f IRCHandler) event.Handler {
h := NewHandler(f)
conn.ER.AddHandler(h, name)
return h
}
// Wrap f in an anonymous unboxing function
func NewHandler(f IRCHandler) event.Handler {
return event.NewHandler(func(ev ...interface{}) {
f(ev[0].(*Conn), ev[1].(*Line))
})
}
// sets up the internal event handlers to do essential IRC protocol things
var intHandlers map[string]event.Handler
func init() {
intHandlers = make(map[string]event.Handler)
intHandlers["001"] = NewHandler((*Conn).h_001)
intHandlers["433"] = NewHandler((*Conn).h_433)
intHandlers["CTCP"] = NewHandler((*Conn).h_CTCP)
intHandlers["NICK"] = NewHandler((*Conn).h_NICK)
intHandlers["PING"] = NewHandler((*Conn).h_PING)
var intHandlers = map[string]HandlerFunc{
"001": (*Conn).h_001,
"433": (*Conn).h_433,
"CTCP": (*Conn).h_CTCP,
"NICK": (*Conn).h_NICK,
"PING": (*Conn).h_PING,
}
func (conn *Conn) addIntHandlers() {
for n, h := range intHandlers {
conn.ER.AddHandler(h, n)
// internal handlers are essential for the IRC client
// to function, so we don't save their Removers here
conn.Handle(n, h)
}
}
@ -57,7 +32,7 @@ func (conn *Conn) h_PING(line *Line) {
// Handler to trigger a "CONNECTED" event on receipt of numeric 001
func (conn *Conn) h_001(line *Line) {
// we're connected!
conn.ED.Dispatch("connected", conn, line)
conn.dispatch(&Line{Cmd: "connected"})
// and we're being given our hostname (from the server's perspective)
t := line.Args[len(line.Args)-1]
if idx := strings.LastIndex(t, " "); idx != -1 {
@ -108,3 +83,35 @@ func (conn *Conn) h_NICK(line *Line) {
conn.Me.Nick = line.Args[0]
}
}
// 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())
}
}

View File

@ -2,8 +2,10 @@ package client
import (
"code.google.com/p/gomock/gomock"
"fmt"
"github.com/fluffle/goirc/state"
"testing"
"time"
)
// This test performs a simple end-to-end verification of correct line parsing
@ -11,14 +13,10 @@ import (
// in this file will call their respective handlers synchronously, otherwise
// testing becomes more difficult.
func TestPING(t *testing.T) {
c, s := setUp(t)
_, s := setUp(t)
defer s.tearDown()
// As this is a real end-to-end test, we need a real end-to-end dispatcher.
c.ED = c.ER
s.nc.Send("PING :1234567890")
s.nc.Expect("PONG :1234567890")
// Return mock dispatcher to it's rightful place afterwards for tearDown.
c.ED = s.ed
}
// Test the handler for 001 / RPL_WELCOME
@ -27,9 +25,18 @@ func Test001(t *testing.T) {
defer s.tearDown()
l := parseLine(":irc.server.org 001 test :Welcome to IRC test!ident@somehost.com")
s.ed.EXPECT().Dispatch("connected", c, l)
// Set up a handler to detect whether connected handler is called from 001
hcon := false
c.HandleFunc("connected", func (conn *Conn, line *Line) {
hcon = true
})
// Call handler with a valid 001 line
c.h_001(l)
<-time.After(time.Millisecond)
if !hcon {
t.Errorf("001 handler did not dispatch connected event.")
}
// Check host parsed correctly
if c.Me.Host != "somehost.com" {
@ -132,6 +139,58 @@ func TestCTCP(t *testing.T) {
c.h_CTCP(parseLine(":blah!moo@cows.com PRIVMSG test :\001UNKNOWN ctcp\001"))
}
func TestPRIVMSG(t *testing.T){
c, s := setUp(t)
defer s.tearDown()
f := func (conn *Conn, line *Line) {
conn.Privmsg(line.Args[0], line.Args[1])
}
c.CommandFunc("prefix", f, "")
// CommandStripNick and CommandStripPrefix are both false to begin
c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :prefix bar"))
s.nc.Expect("PRIVMSG #foo :prefix bar")
// If we're not stripping off the nick, the prefix won't match.
// This might be considered a bug, but then the library currently has a
// poor understanding of the concept of "being addressed".
c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :test: prefix bar"))
s.nc.ExpectNothing()
c.CommandStripNick = true
c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :prefix bar"))
s.nc.Expect("PRIVMSG #foo :prefix bar")
c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :test: prefix bar"))
s.nc.Expect("PRIVMSG #foo :prefix bar")
c.CommandStripPrefix = true
c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :prefix bar"))
s.nc.Expect("PRIVMSG #foo :bar")
c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :test: prefix bar"))
s.nc.Expect("PRIVMSG #foo :bar")
c.CommandStripNick = false
c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :prefix bar"))
s.nc.Expect("PRIVMSG #foo :bar")
c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :test: prefix bar"))
s.nc.ExpectNothing()
// Check the various nick addressing notations that are supported.
c.CommandStripNick = true
for _, addr := range []string{":", ";", ",", ">", "-", ""} {
c.h_PRIVMSG(parseLine(fmt.Sprintf(
":blah!moo@cows.com PRIVMSG #foo :test%s prefix bar", addr)))
s.nc.Expect("PRIVMSG #foo :bar")
c.h_PRIVMSG(parseLine(fmt.Sprintf(
":blah!moo@cows.com PRIVMSG #foo :test%sprefix bar", addr)))
s.nc.ExpectNothing()
}
c.h_PRIVMSG(parseLine(":blah!moo@cows.com PRIVMSG #foo :test! prefix bar"))
s.nc.ExpectNothing()
}
// Test the handler for JOIN messages
func TestJOIN(t *testing.T) {
c, s := setUp(t)

View File

@ -74,7 +74,7 @@ func (m *mockNetConn) Send(s string) {
func (m *mockNetConn) Expect(e string) {
select {
case <-time.After(1e6):
case <-time.After(time.Millisecond):
m.Errorf("Mock connection did not receive expected output.\n\t"+
"Expected: '%s', got nothing.", e)
case s := <-m.Out:
@ -88,7 +88,7 @@ func (m *mockNetConn) Expect(e string) {
func (m *mockNetConn) ExpectNothing() {
select {
case <-time.After(1e6):
case <-time.After(time.Millisecond):
case s := <-m.Out:
s = strings.Trim(s, "\r\n")
m.Errorf("Mock connection received unexpected output.\n\t"+

View File

@ -4,40 +4,37 @@ package client
// to manage tracking state for an IRC connection
import (
"github.com/fluffle/goevent/event"
"github.com/fluffle/golog/logging"
"strings"
)
var stHandlers map[string]event.Handler
func init() {
stHandlers = make(map[string]event.Handler)
stHandlers["JOIN"] = NewHandler((*Conn).h_JOIN)
stHandlers["KICK"] = NewHandler((*Conn).h_KICK)
stHandlers["MODE"] = NewHandler((*Conn).h_MODE)
stHandlers["NICK"] = NewHandler((*Conn).h_STNICK)
stHandlers["PART"] = NewHandler((*Conn).h_PART)
stHandlers["QUIT"] = NewHandler((*Conn).h_QUIT)
stHandlers["TOPIC"] = NewHandler((*Conn).h_TOPIC)
stHandlers["311"] = NewHandler((*Conn).h_311)
stHandlers["324"] = NewHandler((*Conn).h_324)
stHandlers["332"] = NewHandler((*Conn).h_332)
stHandlers["352"] = NewHandler((*Conn).h_352)
stHandlers["353"] = NewHandler((*Conn).h_353)
stHandlers["671"] = NewHandler((*Conn).h_671)
var stHandlers = map[string]HandlerFunc{
"JOIN": (*Conn).h_JOIN,
"KICK": (*Conn).h_KICK,
"MODE": (*Conn).h_MODE,
"NICK": (*Conn).h_STNICK,
"PART": (*Conn).h_PART,
"QUIT": (*Conn).h_QUIT,
"TOPIC": (*Conn).h_TOPIC,
"311": (*Conn).h_311,
"324": (*Conn).h_324,
"332": (*Conn).h_332,
"352": (*Conn).h_352,
"353": (*Conn).h_353,
"671": (*Conn).h_671,
}
func (conn *Conn) addSTHandlers() {
for n, h := range stHandlers {
conn.ER.AddHandler(h, n)
conn.stRemovers = append(conn.stRemovers, conn.Handle(n, h))
}
}
func (conn *Conn) delSTHandlers() {
for n, h := range stHandlers {
conn.ER.DelHandler(h, n)
for _, h := range conn.stRemovers {
h.Remove()
}
conn.stRemovers = conn.stRemovers[:0]
}
// Handle NICK messages that need to update the state tracker