mirror of https://github.com/fluffle/goirc
Re-work Handlers for IRC events; add Commands.
This commit is contained in:
parent
c9ba668094
commit
91c71da2dd
|
@ -5,7 +5,6 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/fluffle/goevent/event"
|
|
||||||
"github.com/fluffle/goirc/state"
|
"github.com/fluffle/goirc/state"
|
||||||
"github.com/fluffle/golog/logging"
|
"github.com/fluffle/golog/logging"
|
||||||
"net"
|
"net"
|
||||||
|
@ -20,16 +19,14 @@ type Conn struct {
|
||||||
Me *state.Nick
|
Me *state.Nick
|
||||||
Network string
|
Network string
|
||||||
|
|
||||||
// Replaceable function to customise the 433 handler's new nick
|
// Handlers and Commands
|
||||||
NewNick func(string) string
|
handlers *hSet
|
||||||
|
commands *cSet
|
||||||
// Event handler registry and dispatcher
|
|
||||||
ER event.EventRegistry
|
|
||||||
ED event.EventDispatcher
|
|
||||||
|
|
||||||
// State tracker for nicks and channels
|
// State tracker for nicks and channels
|
||||||
ST state.StateTracker
|
ST state.StateTracker
|
||||||
st bool
|
st bool
|
||||||
|
stRemovers []Remover
|
||||||
|
|
||||||
// Use the State field to store external state that handlers might need.
|
// Use the State field to store external state that handlers might need.
|
||||||
// Remember ... you might need locking for this ;-)
|
// Remember ... you might need locking for this ;-)
|
||||||
|
@ -50,9 +47,15 @@ type Conn struct {
|
||||||
SSL bool
|
SSL bool
|
||||||
SSLConfig *tls.Config
|
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.
|
// Client->server ping frequency, in seconds. Defaults to 3m.
|
||||||
PingFreq time.Duration
|
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
|
// Set this to true to disable flood protection and false to re-enable
|
||||||
Flood bool
|
Flood bool
|
||||||
|
|
||||||
|
@ -62,9 +65,9 @@ type Conn struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new IRC connection object, but doesn't connect to anything so
|
// 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.
|
// that you can add event handlers to it. See AddHandler() for details
|
||||||
func SimpleClient(nick string, args ...string) *Conn {
|
func Client(nick string, args ...string) *Conn {
|
||||||
r := event.NewRegistry()
|
logging.InitFromFlags()
|
||||||
ident := "goirc"
|
ident := "goirc"
|
||||||
name := "Powered by GoIRC"
|
name := "Powered by GoIRC"
|
||||||
|
|
||||||
|
@ -74,29 +77,17 @@ func SimpleClient(nick string, args ...string) *Conn {
|
||||||
if len(args) > 1 && args[1] != "" {
|
if len(args) > 1 && args[1] != "" {
|
||||||
name = 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{
|
conn := &Conn{
|
||||||
ER: r,
|
|
||||||
ED: r,
|
|
||||||
st: false,
|
|
||||||
in: make(chan *Line, 32),
|
in: make(chan *Line, 32),
|
||||||
out: make(chan string, 32),
|
out: make(chan string, 32),
|
||||||
cSend: make(chan bool),
|
cSend: make(chan bool),
|
||||||
cLoop: make(chan bool),
|
cLoop: make(chan bool),
|
||||||
cPing: make(chan bool),
|
cPing: make(chan bool),
|
||||||
SSL: false,
|
handlers: handlerSet(),
|
||||||
SSLConfig: nil,
|
commands: commandSet(),
|
||||||
|
stRemovers: make([]Remover, 0, len(stHandlers)),
|
||||||
PingFreq: 3 * time.Minute,
|
PingFreq: 3 * time.Minute,
|
||||||
Flood: false,
|
|
||||||
NewNick: func(s string) string { return s + "_" },
|
NewNick: func(s string) string { return s + "_" },
|
||||||
badness: 0,
|
|
||||||
lastsent: time.Now(),
|
lastsent: time.Now(),
|
||||||
}
|
}
|
||||||
conn.addIntHandlers()
|
conn.addIntHandlers()
|
||||||
|
@ -257,7 +248,7 @@ func (conn *Conn) runLoop() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case line := <-conn.in:
|
case line := <-conn.in:
|
||||||
conn.ED.Dispatch(line.Cmd, conn, line)
|
conn.dispatch(line)
|
||||||
case <-conn.cLoop:
|
case <-conn.cLoop:
|
||||||
// strobe on control channel, bail out
|
// strobe on control channel, bail out
|
||||||
return
|
return
|
||||||
|
@ -314,7 +305,7 @@ func (conn *Conn) shutdown() {
|
||||||
// as calling sock.Close() will cause recv() to recieve EOF in readstring()
|
// as calling sock.Close() will cause recv() to recieve EOF in readstring()
|
||||||
if conn.Connected {
|
if conn.Connected {
|
||||||
logging.Info("irc.shutdown(): Disconnected from server.")
|
logging.Info("irc.shutdown(): Disconnected from server.")
|
||||||
conn.ED.Dispatch("disconnected", conn, &Line{})
|
conn.dispatch(&Line{Cmd: "disconnected"})
|
||||||
conn.Connected = false
|
conn.Connected = false
|
||||||
conn.sock.Close()
|
conn.sock.Close()
|
||||||
conn.cSend <- true
|
conn.cSend <- true
|
||||||
|
|
|
@ -3,7 +3,6 @@ package client
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"code.google.com/p/gomock/gomock"
|
"code.google.com/p/gomock/gomock"
|
||||||
"github.com/fluffle/goevent/event"
|
|
||||||
"github.com/fluffle/golog/logging"
|
"github.com/fluffle/golog/logging"
|
||||||
"github.com/fluffle/goirc/state"
|
"github.com/fluffle/goirc/state"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -14,7 +13,6 @@ import (
|
||||||
type testState struct {
|
type testState struct {
|
||||||
ctrl *gomock.Controller
|
ctrl *gomock.Controller
|
||||||
st *state.MockStateTracker
|
st *state.MockStateTracker
|
||||||
ed *event.MockEventDispatcher
|
|
||||||
nc *mockNetConn
|
nc *mockNetConn
|
||||||
c *Conn
|
c *Conn
|
||||||
}
|
}
|
||||||
|
@ -22,13 +20,10 @@ type testState struct {
|
||||||
func setUp(t *testing.T, start ...bool) (*Conn, *testState) {
|
func setUp(t *testing.T, start ...bool) (*Conn, *testState) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
st := state.NewMockStateTracker(ctrl)
|
st := state.NewMockStateTracker(ctrl)
|
||||||
r := event.NewRegistry()
|
|
||||||
ed := event.NewMockEventDispatcher(ctrl)
|
|
||||||
nc := MockNetConn(t)
|
nc := MockNetConn(t)
|
||||||
c := Client("test", "test", "Testing IRC", r)
|
c := Client("test", "test", "Testing IRC")
|
||||||
logging.SetLogLevel(logging.LogFatal)
|
logging.SetLogLevel(logging.LogFatal)
|
||||||
|
|
||||||
c.ED = ed
|
|
||||||
c.ST = st
|
c.ST = st
|
||||||
c.st = true
|
c.st = true
|
||||||
c.sock = nc
|
c.sock = nc
|
||||||
|
@ -42,15 +37,14 @@ func setUp(t *testing.T, start ...bool) (*Conn, *testState) {
|
||||||
<-time.After(1e6)
|
<-time.After(1e6)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, &testState{ctrl, st, ed, nc, c}
|
return c, &testState{ctrl, st, nc, c}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *testState) tearDown() {
|
func (s *testState) tearDown() {
|
||||||
s.ed.EXPECT().Dispatch("disconnected", s.c, &Line{})
|
|
||||||
s.st.EXPECT().Wipe()
|
s.st.EXPECT().Wipe()
|
||||||
s.nc.ExpectNothing()
|
s.nc.ExpectNothing()
|
||||||
s.c.shutdown()
|
s.c.shutdown()
|
||||||
<-time.After(1e6)
|
<-time.After(time.Millisecond)
|
||||||
s.ctrl.Finish()
|
s.ctrl.Finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,56 +55,61 @@ func TestEOF(t *testing.T) {
|
||||||
// Since we're not using tearDown() here, manually call Finish()
|
// Since we're not using tearDown() here, manually call Finish()
|
||||||
defer s.ctrl.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
|
// Simulate EOF from server
|
||||||
s.ed.EXPECT().Dispatch("disconnected", c, &Line{})
|
|
||||||
s.st.EXPECT().Wipe()
|
s.st.EXPECT().Wipe()
|
||||||
s.nc.Close()
|
s.nc.Close()
|
||||||
|
|
||||||
// Since things happen in different internal goroutines, we need to wait
|
// Since things happen in different internal goroutines, we need to wait
|
||||||
// 1 ms should be enough :-)
|
// 1 ms should be enough :-)
|
||||||
<-time.After(1e6)
|
<-time.After(time.Millisecond)
|
||||||
|
|
||||||
// Verify that the connection no longer thinks it's connected
|
// Verify that the connection no longer thinks it's connected
|
||||||
if c.Connected {
|
if c.Connected {
|
||||||
t.Errorf("Conn still thinks it's connected to the server.")
|
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) {
|
func TestClientAndStateTracking(t *testing.T) {
|
||||||
// This doesn't use setUp() as we want to pass in a mock EventRegistry.
|
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
r := event.NewMockEventRegistry(ctrl)
|
|
||||||
st := state.NewMockStateTracker(ctrl)
|
st := state.NewMockStateTracker(ctrl)
|
||||||
|
c := Client("test", "test", "Testing IRC")
|
||||||
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)
|
|
||||||
|
|
||||||
// Assert some basic things about the initial state of the Conn struct
|
// 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" ||
|
if c.Me.Nick != "test" || c.Me.Ident != "test" ||
|
||||||
c.Me.Name != "Testing IRC" || c.Me.Host != "" {
|
c.Me.Name != "Testing IRC" || c.Me.Host != "" {
|
||||||
t.Errorf("Conn.Me not correctly initialised.")
|
t.Errorf("Conn.Me not correctly initialised.")
|
||||||
}
|
}
|
||||||
|
// Check that the internal handlers are correctly set up
|
||||||
// OK, while we're here with a mock event registry...
|
for k, _ := range intHandlers {
|
||||||
for n, _ := range stHandlers {
|
if _, ok := c.handlers.set[strings.ToLower(k)]; !ok {
|
||||||
// See above.
|
t.Errorf("Missing internal handler for '%s'.", k)
|
||||||
ctrl.RecordCall(r, "AddHandler", gomock.Any(), n)
|
}
|
||||||
}
|
}
|
||||||
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" ||
|
if c.Me.Nick != "test" || c.Me.Ident != "test" ||
|
||||||
c.Me.Name != "Testing IRC" || c.Me.Host != "" {
|
c.Me.Name != "Testing IRC" || c.Me.Host != "" {
|
||||||
t.Errorf("Enabling state tracking did not replace Me correctly.")
|
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.")
|
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
|
me := c.Me
|
||||||
c.ST = st
|
c.ST = st
|
||||||
st.EXPECT().Wipe()
|
st.EXPECT().Wipe()
|
||||||
for n, _ := range stHandlers {
|
|
||||||
// See above.
|
|
||||||
ctrl.RecordCall(r, "DelHandler", gomock.Any(), n)
|
|
||||||
}
|
|
||||||
c.DisableStateTracking()
|
c.DisableStateTracking()
|
||||||
if c.st || c.ST != nil || c.Me != me {
|
if c.st || c.ST != nil || c.Me != me {
|
||||||
t.Errorf("State tracker not disabled correctly.")
|
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()
|
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 is a helper to do a "non-blocking" read of c.in
|
||||||
reader := func() *Line {
|
reader := func() *Line {
|
||||||
select {
|
select {
|
||||||
case <-time.After(1e6):
|
case <-time.After(time.Millisecond):
|
||||||
case l := <-c.in:
|
case l := <-c.in:
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
@ -221,7 +227,7 @@ func TestRecv(t *testing.T) {
|
||||||
|
|
||||||
// Strangely, recv() needs some time to start up, but *only* when this test
|
// Strangely, recv() needs some time to start up, but *only* when this test
|
||||||
// is run standalone with: client/_test/_testmain --test.run TestRecv
|
// 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
|
// Now, this should mean that we'll receive our parsed line on c.in
|
||||||
if l := reader(); l == nil || l.Cmd != "001" {
|
if l := reader(); l == nil || l.Cmd != "001" {
|
||||||
|
@ -245,7 +251,6 @@ func TestRecv(t *testing.T) {
|
||||||
if exited {
|
if exited {
|
||||||
t.Errorf("Exited before socket close.")
|
t.Errorf("Exited before socket close.")
|
||||||
}
|
}
|
||||||
s.ed.EXPECT().Dispatch("disconnected", c, &Line{})
|
|
||||||
s.st.EXPECT().Wipe()
|
s.st.EXPECT().Wipe()
|
||||||
s.nc.Close()
|
s.nc.Close()
|
||||||
|
|
||||||
|
@ -255,7 +260,7 @@ func TestRecv(t *testing.T) {
|
||||||
<-c.cLoop
|
<-c.cLoop
|
||||||
<-c.cPing
|
<-c.cPing
|
||||||
// Give things time to shake themselves out...
|
// Give things time to shake themselves out...
|
||||||
<-time.After(1e6)
|
<-time.After(time.Millisecond)
|
||||||
if !exited {
|
if !exited {
|
||||||
t.Errorf("Didn't exit on socket close.")
|
t.Errorf("Didn't exit on socket close.")
|
||||||
}
|
}
|
||||||
|
@ -296,7 +301,7 @@ func TestPing(t *testing.T) {
|
||||||
exited = true
|
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
|
// so we don't expect anything now on c.in
|
||||||
if s := reader(); s != "" {
|
if s := reader(); s != "" {
|
||||||
t.Errorf("Line output directly after ping started.")
|
t.Errorf("Line output directly after ping started.")
|
||||||
|
@ -349,15 +354,25 @@ func TestRunLoop(t *testing.T) {
|
||||||
bufio.NewReader(c.sock),
|
bufio.NewReader(c.sock),
|
||||||
bufio.NewWriter(c.sock))
|
bufio.NewWriter(c.sock))
|
||||||
|
|
||||||
// NOTE: here we assert that no Dispatch event has been called yet by
|
// Set up a handler to detect whether 001 handler is called
|
||||||
// calling s.ctrl.Finish(). There doesn't appear to be any harm in this.
|
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.")
|
l1 := parseLine(":irc.server.org 001 test :First test line.")
|
||||||
c.in <- l1
|
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.
|
// 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.
|
// Now, we can expect the call to Dispatch to take place as runLoop starts.
|
||||||
s.ed.EXPECT().Dispatch("001", c, l1)
|
|
||||||
exited := false
|
exited := false
|
||||||
go func() {
|
go func() {
|
||||||
c.runLoop()
|
c.runLoop()
|
||||||
|
@ -365,15 +380,20 @@ func TestRunLoop(t *testing.T) {
|
||||||
}()
|
}()
|
||||||
// Here, the opposite seemed to take place, with TestRunLoop failing when
|
// 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.
|
// 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 :-)
|
// Send another line, just to be sure :-)
|
||||||
l2 := parseLine(":irc.server.org 002 test :Second test line.")
|
l2 := parseLine(":irc.server.org 002 test :Second test line.")
|
||||||
s.ed.EXPECT().Dispatch("002", c, l2)
|
|
||||||
c.in <- l2
|
c.in <- l2
|
||||||
// It appears some sleeping is needed after all of these to ensure channel
|
// It appears some sleeping is needed after all of these to ensure channel
|
||||||
// sends occur before the close signal is sent below...
|
// 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.
|
// Now, use the control channel to exit send and kill the goroutine.
|
||||||
if exited {
|
if exited {
|
||||||
|
@ -381,13 +401,17 @@ func TestRunLoop(t *testing.T) {
|
||||||
}
|
}
|
||||||
c.cLoop <- true
|
c.cLoop <- true
|
||||||
// Allow propagation time...
|
// Allow propagation time...
|
||||||
<-time.After(1e6)
|
<-time.After(time.Millisecond)
|
||||||
if !exited {
|
if !exited {
|
||||||
t.Errorf("Didn't exit after signal.")
|
t.Errorf("Didn't exit after signal.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sending more on c.in shouldn't dispatch any further events
|
// Sending more on c.in shouldn't dispatch any further events
|
||||||
|
h001 = false
|
||||||
c.in <- l1
|
c.in <- l1
|
||||||
|
if h001 {
|
||||||
|
t.Errorf("001 handler called after runLoop ended.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWrite(t *testing.T) {
|
func TestWrite(t *testing.T) {
|
||||||
|
@ -432,8 +456,6 @@ func TestWrite(t *testing.T) {
|
||||||
<-c.cPing
|
<-c.cPing
|
||||||
}()
|
}()
|
||||||
s.nc.Close()
|
s.nc.Close()
|
||||||
|
|
||||||
s.ed.EXPECT().Dispatch("disconnected", c, &Line{})
|
|
||||||
s.st.EXPECT().Wipe()
|
s.st.EXPECT().Wipe()
|
||||||
c.write("she can't pass unit tests")
|
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
|
// 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
|
// 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.
|
// 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.")
|
t.Errorf("Rate limit calculating badness incorrectly.")
|
||||||
}
|
}
|
||||||
// At this point, we can tip over the badness scale, with a bit of help.
|
// 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
|
// 720 chars => +8 seconds of badness => 10.5 seconds => ratelimit
|
||||||
if l := c.rateLimit(720); l != 8 * time.Second ||
|
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("Rate limit failed to return correct limiting values.")
|
||||||
t.Errorf("l=%d, badness=%d", l, c.badness)
|
t.Errorf("l=%d, badness=%d", l, c.badness)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,48 +4,23 @@ package client
|
||||||
// to manage tracking an irc connection etc.
|
// to manage tracking an irc connection etc.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/fluffle/goevent/event"
|
|
||||||
"strings"
|
"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
|
// sets up the internal event handlers to do essential IRC protocol things
|
||||||
var intHandlers map[string]event.Handler
|
var intHandlers = map[string]HandlerFunc{
|
||||||
func init() {
|
"001": (*Conn).h_001,
|
||||||
intHandlers = make(map[string]event.Handler)
|
"433": (*Conn).h_433,
|
||||||
intHandlers["001"] = NewHandler((*Conn).h_001)
|
"CTCP": (*Conn).h_CTCP,
|
||||||
intHandlers["433"] = NewHandler((*Conn).h_433)
|
"NICK": (*Conn).h_NICK,
|
||||||
intHandlers["CTCP"] = NewHandler((*Conn).h_CTCP)
|
"PING": (*Conn).h_PING,
|
||||||
intHandlers["NICK"] = NewHandler((*Conn).h_NICK)
|
|
||||||
intHandlers["PING"] = NewHandler((*Conn).h_PING)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *Conn) addIntHandlers() {
|
func (conn *Conn) addIntHandlers() {
|
||||||
for n, h := range intHandlers {
|
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
|
// Handler to trigger a "CONNECTED" event on receipt of numeric 001
|
||||||
func (conn *Conn) h_001(line *Line) {
|
func (conn *Conn) h_001(line *Line) {
|
||||||
// we're connected!
|
// 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)
|
// and we're being given our hostname (from the server's perspective)
|
||||||
t := line.Args[len(line.Args)-1]
|
t := line.Args[len(line.Args)-1]
|
||||||
if idx := strings.LastIndex(t, " "); idx != -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]
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,8 +2,10 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.google.com/p/gomock/gomock"
|
"code.google.com/p/gomock/gomock"
|
||||||
|
"fmt"
|
||||||
"github.com/fluffle/goirc/state"
|
"github.com/fluffle/goirc/state"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This test performs a simple end-to-end verification of correct line parsing
|
// 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
|
// in this file will call their respective handlers synchronously, otherwise
|
||||||
// testing becomes more difficult.
|
// testing becomes more difficult.
|
||||||
func TestPING(t *testing.T) {
|
func TestPING(t *testing.T) {
|
||||||
c, s := setUp(t)
|
_, s := setUp(t)
|
||||||
defer s.tearDown()
|
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.Send("PING :1234567890")
|
||||||
s.nc.Expect("PONG :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
|
// Test the handler for 001 / RPL_WELCOME
|
||||||
|
@ -27,9 +25,18 @@ func Test001(t *testing.T) {
|
||||||
defer s.tearDown()
|
defer s.tearDown()
|
||||||
|
|
||||||
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")
|
||||||
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
|
// Call handler with a valid 001 line
|
||||||
c.h_001(l)
|
c.h_001(l)
|
||||||
|
<-time.After(time.Millisecond)
|
||||||
|
if !hcon {
|
||||||
|
t.Errorf("001 handler did not dispatch connected event.")
|
||||||
|
}
|
||||||
|
|
||||||
// Check host parsed correctly
|
// Check host parsed correctly
|
||||||
if c.Me.Host != "somehost.com" {
|
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"))
|
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
|
// Test the handler for JOIN messages
|
||||||
func TestJOIN(t *testing.T) {
|
func TestJOIN(t *testing.T) {
|
||||||
c, s := setUp(t)
|
c, s := setUp(t)
|
||||||
|
|
|
@ -74,7 +74,7 @@ func (m *mockNetConn) Send(s string) {
|
||||||
|
|
||||||
func (m *mockNetConn) Expect(e string) {
|
func (m *mockNetConn) Expect(e string) {
|
||||||
select {
|
select {
|
||||||
case <-time.After(1e6):
|
case <-time.After(time.Millisecond):
|
||||||
m.Errorf("Mock connection did not receive expected output.\n\t"+
|
m.Errorf("Mock connection did not receive expected output.\n\t"+
|
||||||
"Expected: '%s', got nothing.", e)
|
"Expected: '%s', got nothing.", e)
|
||||||
case s := <-m.Out:
|
case s := <-m.Out:
|
||||||
|
@ -88,7 +88,7 @@ func (m *mockNetConn) Expect(e string) {
|
||||||
|
|
||||||
func (m *mockNetConn) ExpectNothing() {
|
func (m *mockNetConn) ExpectNothing() {
|
||||||
select {
|
select {
|
||||||
case <-time.After(1e6):
|
case <-time.After(time.Millisecond):
|
||||||
case s := <-m.Out:
|
case s := <-m.Out:
|
||||||
s = strings.Trim(s, "\r\n")
|
s = strings.Trim(s, "\r\n")
|
||||||
m.Errorf("Mock connection received unexpected output.\n\t"+
|
m.Errorf("Mock connection received unexpected output.\n\t"+
|
||||||
|
|
|
@ -4,40 +4,37 @@ package client
|
||||||
// to manage tracking state for an IRC connection
|
// to manage tracking state for an IRC connection
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/fluffle/goevent/event"
|
|
||||||
"github.com/fluffle/golog/logging"
|
"github.com/fluffle/golog/logging"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var stHandlers map[string]event.Handler
|
var stHandlers = map[string]HandlerFunc{
|
||||||
|
"JOIN": (*Conn).h_JOIN,
|
||||||
func init() {
|
"KICK": (*Conn).h_KICK,
|
||||||
stHandlers = make(map[string]event.Handler)
|
"MODE": (*Conn).h_MODE,
|
||||||
stHandlers["JOIN"] = NewHandler((*Conn).h_JOIN)
|
"NICK": (*Conn).h_STNICK,
|
||||||
stHandlers["KICK"] = NewHandler((*Conn).h_KICK)
|
"PART": (*Conn).h_PART,
|
||||||
stHandlers["MODE"] = NewHandler((*Conn).h_MODE)
|
"QUIT": (*Conn).h_QUIT,
|
||||||
stHandlers["NICK"] = NewHandler((*Conn).h_STNICK)
|
"TOPIC": (*Conn).h_TOPIC,
|
||||||
stHandlers["PART"] = NewHandler((*Conn).h_PART)
|
"311": (*Conn).h_311,
|
||||||
stHandlers["QUIT"] = NewHandler((*Conn).h_QUIT)
|
"324": (*Conn).h_324,
|
||||||
stHandlers["TOPIC"] = NewHandler((*Conn).h_TOPIC)
|
"332": (*Conn).h_332,
|
||||||
stHandlers["311"] = NewHandler((*Conn).h_311)
|
"352": (*Conn).h_352,
|
||||||
stHandlers["324"] = NewHandler((*Conn).h_324)
|
"353": (*Conn).h_353,
|
||||||
stHandlers["332"] = NewHandler((*Conn).h_332)
|
"671": (*Conn).h_671,
|
||||||
stHandlers["352"] = NewHandler((*Conn).h_352)
|
|
||||||
stHandlers["353"] = NewHandler((*Conn).h_353)
|
|
||||||
stHandlers["671"] = NewHandler((*Conn).h_671)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *Conn) addSTHandlers() {
|
func (conn *Conn) addSTHandlers() {
|
||||||
for n, h := range stHandlers {
|
for n, h := range stHandlers {
|
||||||
conn.ER.AddHandler(h, n)
|
conn.stRemovers = append(conn.stRemovers, conn.Handle(n, h))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *Conn) delSTHandlers() {
|
func (conn *Conn) delSTHandlers() {
|
||||||
for n, h := range stHandlers {
|
for _, h := range conn.stRemovers {
|
||||||
conn.ER.DelHandler(h, n)
|
h.Remove()
|
||||||
}
|
}
|
||||||
|
conn.stRemovers = conn.stRemovers[:0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle NICK messages that need to update the state tracker
|
// Handle NICK messages that need to update the state tracker
|
||||||
|
|
Loading…
Reference in New Issue