summaryrefslogtreecommitdiff
path: root/vendor/github.com/fluffle/goirc/client
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/fluffle/goirc/client')
-rw-r--r--vendor/github.com/fluffle/goirc/client/commands.go304
-rw-r--r--vendor/github.com/fluffle/goirc/client/commands_test.go205
-rw-r--r--vendor/github.com/fluffle/goirc/client/connection.go581
-rw-r--r--vendor/github.com/fluffle/goirc/client/connection_test.go585
-rw-r--r--vendor/github.com/fluffle/goirc/client/dispatch.go202
-rw-r--r--vendor/github.com/fluffle/goirc/client/dispatch_test.go201
-rw-r--r--vendor/github.com/fluffle/goirc/client/doc.go34
-rw-r--r--vendor/github.com/fluffle/goirc/client/handlers.go105
-rw-r--r--vendor/github.com/fluffle/goirc/client/handlers_test.go451
-rw-r--r--vendor/github.com/fluffle/goirc/client/line.go216
-rw-r--r--vendor/github.com/fluffle/goirc/client/line_test.go186
-rw-r--r--vendor/github.com/fluffle/goirc/client/mocknetconn_test.go154
-rw-r--r--vendor/github.com/fluffle/goirc/client/state_handlers.go262
13 files changed, 3486 insertions, 0 deletions
diff --git a/vendor/github.com/fluffle/goirc/client/commands.go b/vendor/github.com/fluffle/goirc/client/commands.go
new file mode 100644
index 0000000..101c7d3
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/commands.go
@@ -0,0 +1,304 @@
+package client
+
+import (
+ "fmt"
+ "strings"
+)
+
+const (
+ REGISTER = "REGISTER"
+ CONNECTED = "CONNECTED"
+ DISCONNECTED = "DISCONNECTED"
+ ACTION = "ACTION"
+ AWAY = "AWAY"
+ CAP = "CAP"
+ CTCP = "CTCP"
+ CTCPREPLY = "CTCPREPLY"
+ ERROR = "ERROR"
+ 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"
+ VHOST = "VHOST"
+ WHO = "WHO"
+ WHOIS = "WHOIS"
+ defaultSplit = 450
+)
+
+// cutNewLines() pares down a string to the part before the first "\r" or "\n".
+func cutNewLines(s string) string {
+ r := strings.SplitN(s, "\r", 2)
+ r = strings.SplitN(r[0], "\n", 2)
+ return r[0]
+}
+
+// indexFragment looks for the last sentence split-point (defined as one of
+// the punctuation characters .:;,!?"' followed by a space) in the string s
+// and returns the index in the string after that split-point. If no split-
+// point is found it returns the index after the last space in s, or -1.
+func indexFragment(s string) int {
+ max := -1
+ for _, sep := range []string{". ", ": ", "; ", ", ", "! ", "? ", "\" ", "' "} {
+ if idx := strings.LastIndex(s, sep); idx > max {
+ max = idx
+ }
+ }
+ if max > 0 {
+ return max + 2
+ }
+ if idx := strings.LastIndex(s, " "); idx > 0 {
+ return idx + 1
+ }
+ return -1
+}
+
+// splitMessage splits a message > splitLen chars at:
+// 1. the end of the last sentence fragment before splitLen
+// 2. the end of the last word before splitLen
+// 3. splitLen itself
+func splitMessage(msg string, splitLen int) (msgs []string) {
+ // This is quite short ;-)
+ if splitLen < 13 {
+ splitLen = defaultSplit
+ }
+ for len(msg) > splitLen {
+ idx := indexFragment(msg[:splitLen-3])
+ if idx < 0 {
+ idx = splitLen - 3
+ }
+ msgs = append(msgs, msg[:idx]+"...")
+ msg = msg[idx:]
+ }
+ return append(msgs, msg)
+}
+
+// Raw sends a raw line to the server, should really only be used for
+// debugging purposes but may well come in handy.
+func (conn *Conn) Raw(rawline string) {
+ // Avoid command injection by enforcing one command per line.
+ conn.out <- cutNewLines(rawline)
+}
+
+// Pass sends a PASS command to the server.
+// PASS password
+func (conn *Conn) Pass(password string) { conn.Raw(PASS + " " + password) }
+
+// Nick sends a NICK command to the server.
+// NICK nick
+func (conn *Conn) Nick(nick string) { conn.Raw(NICK + " " + nick) }
+
+// User sends a USER command to the server.
+// USER ident 12 * :name
+func (conn *Conn) User(ident, name string) {
+ conn.Raw(USER + " " + ident + " 12 * :" + name)
+}
+
+// Join sends a JOIN command to the server with an optional key.
+// JOIN channel [key]
+func (conn *Conn) Join(channel string, key ...string) {
+ k := ""
+ if len(key) > 0 {
+ k = " " + key[0]
+ }
+ conn.Raw(JOIN + " " + channel + k)
+}
+
+// Part sends a PART command to the server with an optional part message.
+// PART channel [:message]
+func (conn *Conn) Part(channel string, message ...string) {
+ msg := strings.Join(message, " ")
+ if msg != "" {
+ msg = " :" + msg
+ }
+ conn.Raw(PART + " " + channel + msg)
+}
+
+// Kick sends a KICK command to remove a nick from a channel.
+// KICK channel nick [:message]
+func (conn *Conn) Kick(channel, nick string, message ...string) {
+ msg := strings.Join(message, " ")
+ if msg != "" {
+ msg = " :" + msg
+ }
+ conn.Raw(KICK + " " + channel + " " + nick + msg)
+}
+
+// Quit sends a QUIT command to the server with an optional quit message.
+// QUIT [:message]
+func (conn *Conn) Quit(message ...string) {
+ msg := strings.Join(message, " ")
+ if msg == "" {
+ msg = conn.cfg.QuitMessage
+ }
+ conn.Raw(QUIT + " :" + msg)
+}
+
+// Whois sends a WHOIS command to the server.
+// WHOIS nick
+func (conn *Conn) Whois(nick string) { conn.Raw(WHOIS + " " + nick) }
+
+// Who sends a WHO command to the server.
+// WHO nick
+func (conn *Conn) Who(nick string) { conn.Raw(WHO + " " + nick) }
+
+// Privmsg sends a PRIVMSG to the target nick or channel t.
+// If msg is longer than Config.SplitLen characters, multiple PRIVMSGs
+// will be sent to the target containing sequential parts of msg.
+// PRIVMSG t :msg
+func (conn *Conn) Privmsg(t, msg string) {
+ prefix := PRIVMSG + " " + t + " :"
+ for _, s := range splitMessage(msg, conn.cfg.SplitLen) {
+ conn.Raw(prefix + s)
+ }
+}
+
+// Privmsgln is the variadic version of Privmsg that formats the message
+// that is sent to the target nick or channel t using the
+// fmt.Sprintln function.
+// Note: Privmsgln doesn't add the '\n' character at the end of the message.
+func (conn *Conn) Privmsgln(t string, a ...interface{}) {
+ msg := fmt.Sprintln(a...)
+ // trimming the new-line character added by the fmt.Sprintln function,
+ // since it's irrelevant.
+ msg = msg[:len(msg)-1]
+ conn.Privmsg(t, msg)
+}
+
+// Privmsgf is the variadic version of Privmsg that formats the message
+// that is sent to the target nick or channel t using the
+// fmt.Sprintf function.
+func (conn *Conn) Privmsgf(t, format string, a ...interface{}) {
+ msg := fmt.Sprintf(format, a...)
+ conn.Privmsg(t, msg)
+}
+
+// Notice sends a NOTICE to the target nick or channel t.
+// If msg is longer than Config.SplitLen characters, multiple NOTICEs
+// will be sent to the target containing sequential parts of msg.
+// NOTICE t :msg
+func (conn *Conn) Notice(t, msg string) {
+ for _, s := range splitMessage(msg, conn.cfg.SplitLen) {
+ conn.Raw(NOTICE + " " + t + " :" + s)
+ }
+}
+
+// Ctcp sends a (generic) CTCP message to the target nick
+// or channel t, with an optional argument.
+// PRIVMSG t :\001CTCP arg\001
+func (conn *Conn) Ctcp(t, ctcp string, arg ...string) {
+ // We need to split again here to ensure
+ for _, s := range splitMessage(strings.Join(arg, " "), conn.cfg.SplitLen) {
+ if s != "" {
+ s = " " + s
+ }
+ // Using Raw rather than PRIVMSG here to avoid double-split problems.
+ conn.Raw(PRIVMSG + " " + t + " :\001" + strings.ToUpper(ctcp) + s + "\001")
+ }
+}
+
+// CtcpReply sends a (generic) CTCP reply to the target nick
+// or channel t, with an optional argument.
+// NOTICE t :\001CTCP arg\001
+func (conn *Conn) CtcpReply(t, ctcp string, arg ...string) {
+ for _, s := range splitMessage(strings.Join(arg, " "), conn.cfg.SplitLen) {
+ if s != "" {
+ s = " " + s
+ }
+ // Using Raw rather than NOTICE here to avoid double-split problems.
+ conn.Raw(NOTICE + " " + t + " :\001" + strings.ToUpper(ctcp) + s + "\001")
+ }
+}
+
+// Version sends a CTCP "VERSION" to the target nick or channel t.
+func (conn *Conn) Version(t string) { conn.Ctcp(t, VERSION) }
+
+// Action sends a CTCP "ACTION" to the target nick or channel t.
+func (conn *Conn) Action(t, msg string) { conn.Ctcp(t, ACTION, msg) }
+
+// Topic() sends a TOPIC command for a channel.
+// If no topic is provided this requests that a 332 response is sent by the
+// server for that channel, which can then be handled to retrieve the current
+// channel topic. If a topic is provided the channel's topic will be set.
+// TOPIC channel
+// TOPIC channel :topic
+func (conn *Conn) Topic(channel string, topic ...string) {
+ t := strings.Join(topic, " ")
+ if t != "" {
+ t = " :" + t
+ }
+ conn.Raw(TOPIC + " " + channel + t)
+}
+
+// Mode sends a MODE command for a target nick or channel t.
+// If no mode strings are provided this requests that a 324 response is sent
+// by the server for the target. Otherwise the mode strings are concatenated
+// with spaces and sent to the server. This allows e.g.
+// conn.Mode("#channel", "+nsk", "mykey")
+//
+// MODE t
+// MODE t modestring
+func (conn *Conn) Mode(t string, modestring ...string) {
+ mode := strings.Join(modestring, " ")
+ if mode != "" {
+ mode = " " + mode
+ }
+ conn.Raw(MODE + " " + t + mode)
+}
+
+// Away sends an AWAY command to the server.
+// If a message is provided it sets the client's away status with that message,
+// otherwise it resets the client's away status.
+// AWAY
+// AWAY :message
+func (conn *Conn) Away(message ...string) {
+ msg := strings.Join(message, " ")
+ if msg != "" {
+ msg = " :" + msg
+ }
+ conn.Raw(AWAY + msg)
+}
+
+// Invite sends an INVITE command to the server.
+// INVITE nick channel
+func (conn *Conn) Invite(nick, channel string) {
+ conn.Raw(INVITE + " " + nick + " " + channel)
+}
+
+// Oper sends an OPER command to the server.
+// OPER user pass
+func (conn *Conn) Oper(user, pass string) { conn.Raw(OPER + " " + user + " " + pass) }
+
+// VHost sends a VHOST command to the server.
+// VHOST user pass
+func (conn *Conn) VHost(user, pass string) { conn.Raw(VHOST + " " + user + " " + pass) }
+
+// Ping sends a PING command to the server, which should PONG.
+// PING :message
+func (conn *Conn) Ping(message string) { conn.Raw(PING + " :" + message) }
+
+// Pong sends a PONG command to the server.
+// PONG :message
+func (conn *Conn) Pong(message string) { conn.Raw(PONG + " :" + message) }
+
+// Cap sends a CAP command to the server.
+// CAP subcommand
+// CAP subcommand :message
+func (conn *Conn) Cap(subcommmand string, capabilities ...string) {
+ if len(capabilities) == 0 {
+ conn.Raw(CAP + " " + subcommmand)
+ } else {
+ conn.Raw(CAP + " " + subcommmand + " :" + strings.Join(capabilities, " "))
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/commands_test.go b/vendor/github.com/fluffle/goirc/client/commands_test.go
new file mode 100644
index 0000000..15a8a05
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/commands_test.go
@@ -0,0 +1,205 @@
+package client
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestCutNewLines(t *testing.T) {
+ tests := []struct{ in, out string }{
+ {"", ""},
+ {"foo bar", "foo bar"},
+ {"foo bar\rbaz", "foo bar"},
+ {"foo bar\nbaz", "foo bar"},
+ {"blorp\r\n\r\nbloop", "blorp"},
+ {"\n\rblaap", ""},
+ {"\r\n", ""},
+ {"boo\\r\\n\\n\r", "boo\\r\\n\\n"},
+ }
+ for i, test := range tests {
+ out := cutNewLines(test.in)
+ if test.out != out {
+ t.Errorf("test %d: expected %q, got %q", i, test.out, out)
+ }
+ }
+}
+
+func TestIndexFragment(t *testing.T) {
+ tests := []struct {
+ in string
+ out int
+ }{
+ {"", -1},
+ {"foobarbaz", -1},
+ {"foo bar baz", 8},
+ {"foo. bar baz", 5},
+ {"foo: bar baz", 5},
+ {"foo; bar baz", 5},
+ {"foo, bar baz", 5},
+ {"foo! bar baz", 5},
+ {"foo? bar baz", 5},
+ {"foo\" bar baz", 5},
+ {"foo' bar baz", 5},
+ {"foo. bar. baz beep", 10},
+ {"foo. bar, baz beep", 10},
+ }
+ for i, test := range tests {
+ out := indexFragment(test.in)
+ if test.out != out {
+ t.Errorf("test %d: expected %d, got %d", i, test.out, out)
+ }
+ }
+}
+
+func TestSplitMessage(t *testing.T) {
+ tests := []struct {
+ in string
+ sp int
+ out []string
+ }{
+ {"", 0, []string{""}},
+ {"foo", 0, []string{"foo"}},
+ {"foo bar baz beep", 0, []string{"foo bar baz beep"}},
+ {"foo bar baz beep", 15, []string{"foo bar baz ...", "beep"}},
+ {"foo bar, baz beep", 15, []string{"foo bar, ...", "baz beep"}},
+ {"0123456789012345", 0, []string{"0123456789012345"}},
+ {"0123456789012345", 15, []string{"012345678901...", "2345"}},
+ {"0123456789012345", 16, []string{"0123456789012345"}},
+ }
+ for i, test := range tests {
+ out := splitMessage(test.in, test.sp)
+ if !reflect.DeepEqual(test.out, out) {
+ t.Errorf("test %d: expected %q, got %q", i, test.out, out)
+ }
+ }
+}
+
+func TestClientCommands(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Avoid having to type ridiculously long lines to test that
+ // messages longer than SplitLen are correctly sent to the server.
+ c.cfg.SplitLen = 23
+
+ c.Pass("password")
+ s.nc.Expect("PASS password")
+
+ c.Nick("test")
+ s.nc.Expect("NICK test")
+
+ c.User("test", "Testing IRC")
+ s.nc.Expect("USER test 12 * :Testing IRC")
+
+ c.Raw("JUST a raw :line")
+ s.nc.Expect("JUST a raw :line")
+
+ c.Join("#foo")
+ s.nc.Expect("JOIN #foo")
+ c.Join("#foo bar")
+ s.nc.Expect("JOIN #foo bar")
+
+ c.Part("#foo")
+ s.nc.Expect("PART #foo")
+ c.Part("#foo", "Screw you guys...")
+ s.nc.Expect("PART #foo :Screw you guys...")
+
+ c.Quit()
+ s.nc.Expect("QUIT :GoBye!")
+ c.Quit("I'm going home.")
+ s.nc.Expect("QUIT :I'm going home.")
+
+ c.Whois("somebody")
+ s.nc.Expect("WHOIS somebody")
+
+ c.Who("*@some.host.com")
+ s.nc.Expect("WHO *@some.host.com")
+
+ c.Privmsg("#foo", "bar")
+ s.nc.Expect("PRIVMSG #foo :bar")
+
+ c.Privmsgln("#foo", "bar")
+ s.nc.Expect("PRIVMSG #foo :bar")
+
+ c.Privmsgf("#foo", "say %s", "foo")
+ s.nc.Expect("PRIVMSG #foo :say foo")
+
+ c.Privmsgln("#foo", "bar", 1, 3.54, []int{24, 36})
+ s.nc.Expect("PRIVMSG #foo :bar 1 3.54 [24 36]")
+
+ c.Privmsgf("#foo", "user %d is at %s", 2, "home")
+ s.nc.Expect("PRIVMSG #foo :user 2 is at home")
+
+ // 0123456789012345678901234567890123
+ c.Privmsg("#foo", "foo bar baz blorp. woo woobly woo.")
+ s.nc.Expect("PRIVMSG #foo :foo bar baz blorp. ...")
+ s.nc.Expect("PRIVMSG #foo :woo woobly woo.")
+
+ c.Privmsgln("#foo", "foo bar baz blorp. woo woobly woo.")
+ s.nc.Expect("PRIVMSG #foo :foo bar baz blorp. ...")
+ s.nc.Expect("PRIVMSG #foo :woo woobly woo.")
+
+ c.Privmsgf("#foo", "%s %s", "foo bar baz blorp.", "woo woobly woo.")
+ s.nc.Expect("PRIVMSG #foo :foo bar baz blorp. ...")
+ s.nc.Expect("PRIVMSG #foo :woo woobly woo.")
+
+ c.Privmsgln("#foo", "foo bar", 3.54, "blorp.", "woo", "woobly", []int{1, 2})
+ s.nc.Expect("PRIVMSG #foo :foo bar 3.54 blorp. ...")
+ s.nc.Expect("PRIVMSG #foo :woo woobly [1 2]")
+
+ c.Privmsgf("#foo", "%s %.2f %s %s %s %v", "foo bar", 3.54, "blorp.", "woo", "woobly", []int{1, 2})
+ s.nc.Expect("PRIVMSG #foo :foo bar 3.54 blorp. ...")
+ s.nc.Expect("PRIVMSG #foo :woo woobly [1 2]")
+
+ c.Notice("somebody", "something")
+ s.nc.Expect("NOTICE somebody :something")
+
+ // 01234567890123456789012345678901234567
+ c.Notice("somebody", "something much much longer that splits")
+ s.nc.Expect("NOTICE somebody :something much much ...")
+ s.nc.Expect("NOTICE somebody :longer that splits")
+
+ c.Ctcp("somebody", "ping", "123456789")
+ s.nc.Expect("PRIVMSG somebody :\001PING 123456789\001")
+
+ c.Ctcp("somebody", "ping", "123456789012345678901234567890")
+ s.nc.Expect("PRIVMSG somebody :\001PING 12345678901234567890...\001")
+ s.nc.Expect("PRIVMSG somebody :\001PING 1234567890\001")
+
+ c.CtcpReply("somebody", "pong", "123456789012345678901234567890")
+ s.nc.Expect("NOTICE somebody :\001PONG 12345678901234567890...\001")
+ s.nc.Expect("NOTICE somebody :\001PONG 1234567890\001")
+
+ c.CtcpReply("somebody", "pong", "123456789")
+ s.nc.Expect("NOTICE somebody :\001PONG 123456789\001")
+
+ c.Version("somebody")
+ s.nc.Expect("PRIVMSG somebody :\001VERSION\001")
+
+ c.Action("#foo", "pokes somebody")
+ s.nc.Expect("PRIVMSG #foo :\001ACTION pokes somebody\001")
+
+ c.Topic("#foo")
+ s.nc.Expect("TOPIC #foo")
+ c.Topic("#foo", "la la la")
+ s.nc.Expect("TOPIC #foo :la la la")
+
+ c.Mode("#foo")
+ s.nc.Expect("MODE #foo")
+ c.Mode("#foo", "+o somebody")
+ s.nc.Expect("MODE #foo +o somebody")
+
+ c.Away()
+ s.nc.Expect("AWAY")
+ c.Away("Dave's not here, man.")
+ s.nc.Expect("AWAY :Dave's not here, man.")
+
+ c.Invite("somebody", "#foo")
+ s.nc.Expect("INVITE somebody #foo")
+
+ c.Oper("user", "pass")
+ s.nc.Expect("OPER user pass")
+
+ c.VHost("user", "pass")
+ s.nc.Expect("VHOST user pass")
+}
diff --git a/vendor/github.com/fluffle/goirc/client/connection.go b/vendor/github.com/fluffle/goirc/client/connection.go
new file mode 100644
index 0000000..f84621a
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/connection.go
@@ -0,0 +1,581 @@
+package client
+
+import (
+ "bufio"
+ "crypto/tls"
+ "fmt"
+ "io"
+ "net"
+ "net/url"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/fluffle/goirc/logging"
+ "github.com/fluffle/goirc/state"
+ "golang.org/x/net/proxy"
+)
+
+// Conn encapsulates a connection to a single IRC server. Create
+// one with Client or SimpleClient.
+type Conn struct {
+ // For preventing races on (dis)connect.
+ mu sync.RWMutex
+
+ // Contains parameters that people can tweak to change client behaviour.
+ cfg *Config
+
+ // Handlers
+ intHandlers *hSet
+ fgHandlers *hSet
+ bgHandlers *hSet
+
+ // State tracker for nicks and channels
+ st state.Tracker
+ stRemovers []Remover
+
+ // I/O stuff to server
+ dialer *net.Dialer
+ proxyDialer proxy.Dialer
+ sock net.Conn
+ io *bufio.ReadWriter
+ in chan *Line
+ out chan string
+ connected bool
+
+ // Control channel and WaitGroup for goroutines
+ die chan struct{}
+ wg sync.WaitGroup
+
+ // Internal counters for flood protection
+ badness time.Duration
+ lastsent time.Time
+}
+
+// Config contains options that can be passed to Client to change the
+// behaviour of the library during use. It is recommended that NewConfig
+// is used to create this struct rather than instantiating one directly.
+// Passing a Config with no Nick in the Me field to Client will result
+// in unflattering consequences.
+type Config struct {
+ // Set this to provide the Nick, Ident and Name for the client to use.
+ // It is recommended to call Conn.Me to get up-to-date information
+ // about the current state of the client's IRC nick after connecting.
+ Me *state.Nick
+
+ // Hostname to connect to and optional connect password.
+ // Changing these after connection will have no effect until the
+ // client reconnects.
+ Server, Pass string
+
+ // Are we connecting via SSL? Do we care about certificate validity?
+ // Changing these after connection will have no effect until the
+ // client reconnects.
+ SSL bool
+ SSLConfig *tls.Config
+
+ // To connect via proxy set the proxy url here.
+ // Changing these after connection will have no effect until the
+ // client reconnects.
+ Proxy string
+
+ // Local address to bind to when connecting to the server.
+ LocalAddr string
+
+ // Replaceable function to customise the 433 handler's new nick.
+ // By default an underscore "_" is appended to the current nick.
+ NewNick func(string) string
+
+ // Client->server ping frequency, in seconds. Defaults to 3m.
+ // Set to 0 to disable client-side pings.
+ PingFreq time.Duration
+
+ // The duration before a connection timeout is triggered. Defaults to 1m.
+ // Set to 0 to wait indefinitely.
+ Timeout time.Duration
+
+ // Set this to true to disable flood protection and false to re-enable.
+ Flood bool
+
+ // Sent as the reply to a CTCP VERSION message.
+ Version string
+
+ // Sent as the default QUIT message if Quit is called with no args.
+ QuitMessage string
+
+ // Configurable panic recovery for all handlers.
+ // Defaults to logging an error, see LogPanic.
+ Recover func(*Conn, *Line)
+
+ // Split PRIVMSGs, NOTICEs and CTCPs longer than SplitLen characters
+ // over multiple lines. Default to 450 if not set.
+ SplitLen int
+}
+
+// NewConfig creates a Config struct containing sensible defaults.
+// It takes one required argument: the nick to use for the client.
+// Subsequent string arguments set the client's ident and "real"
+// name, but these are optional.
+func NewConfig(nick string, args ...string) *Config {
+ cfg := &Config{
+ Me: &state.Nick{Nick: nick},
+ PingFreq: 3 * time.Minute,
+ NewNick: func(s string) string { return s + "_" },
+ Recover: (*Conn).LogPanic, // in dispatch.go
+ SplitLen: defaultSplit,
+ Timeout: 60 * time.Second,
+ }
+ cfg.Me.Ident = "goirc"
+ if len(args) > 0 && args[0] != "" {
+ cfg.Me.Ident = args[0]
+ }
+ cfg.Me.Name = "Powered by GoIRC"
+ if len(args) > 1 && args[1] != "" {
+ cfg.Me.Name = args[1]
+ }
+ cfg.Version = "Powered by GoIRC"
+ cfg.QuitMessage = "GoBye!"
+ return cfg
+}
+
+// SimpleClient creates a new Conn, passing its arguments to NewConfig.
+// If you don't need to change any client options and just want to get
+// started quickly, this is a convenient shortcut.
+func SimpleClient(nick string, args ...string) *Conn {
+ conn := Client(NewConfig(nick, args...))
+ return conn
+}
+
+// Client takes a Config struct and returns a new Conn ready to have
+// handlers added and connect to a server.
+func Client(cfg *Config) *Conn {
+ if cfg == nil {
+ cfg = NewConfig("__idiot__")
+ }
+ if cfg.Me == nil || cfg.Me.Nick == "" || cfg.Me.Ident == "" {
+ cfg.Me = &state.Nick{Nick: "__idiot__"}
+ cfg.Me.Ident = "goirc"
+ cfg.Me.Name = "Powered by GoIRC"
+ }
+
+ dialer := new(net.Dialer)
+ dialer.Timeout = cfg.Timeout
+ if cfg.LocalAddr != "" {
+ if !hasPort(cfg.LocalAddr) {
+ cfg.LocalAddr += ":0"
+ }
+
+ local, err := net.ResolveTCPAddr("tcp", cfg.LocalAddr)
+ if err == nil {
+ dialer.LocalAddr = local
+ } else {
+ logging.Error("irc.Client(): Cannot resolve local address %s: %s", cfg.LocalAddr, err)
+ }
+ }
+
+ conn := &Conn{
+ cfg: cfg,
+ dialer: dialer,
+ intHandlers: handlerSet(),
+ fgHandlers: handlerSet(),
+ bgHandlers: handlerSet(),
+ stRemovers: make([]Remover, 0, len(stHandlers)),
+ lastsent: time.Now(),
+ }
+ conn.addIntHandlers()
+ return conn
+}
+
+// Connected returns true if the client is successfully connected to
+// an IRC server. It becomes true when the TCP connection is established,
+// and false again when the connection is closed.
+func (conn *Conn) Connected() bool {
+ conn.mu.RLock()
+ defer conn.mu.RUnlock()
+ return conn.connected
+}
+
+// Config returns a pointer to the Config struct used by the client.
+// Many of the elements of Config may be changed at any point to
+// affect client behaviour. To disable flood protection temporarily,
+// for example, a handler could do:
+//
+// conn.Config().Flood = true
+// // Send many lines to the IRC server, risking "excess flood"
+// conn.Config().Flood = false
+//
+func (conn *Conn) Config() *Config {
+ return conn.cfg
+}
+
+// Me returns a state.Nick that reflects the client's IRC nick at the
+// time it is called. If state tracking is enabled, this comes from
+// the tracker, otherwise it is equivalent to conn.cfg.Me.
+func (conn *Conn) Me() *state.Nick {
+ if conn.st != nil {
+ conn.cfg.Me = conn.st.Me()
+ }
+ return conn.cfg.Me
+}
+
+// StateTracker returns the state tracker being used by the client,
+// if tracking is enabled, and nil otherwise.
+func (conn *Conn) StateTracker() state.Tracker {
+ return conn.st
+}
+
+// EnableStateTracking causes the client to track information about
+// all channels it is joined to, and all the nicks in those channels.
+// This can be rather handy for a number of bot-writing tasks. See
+// the state package for more details.
+//
+// NOTE: Calling this while connected to an IRC server may cause the
+// state tracker to become very confused all over STDERR if logging
+// is enabled. State tracking should enabled before connecting or
+// at a pinch while the client is not joined to any channels.
+func (conn *Conn) EnableStateTracking() {
+ conn.mu.Lock()
+ defer conn.mu.Unlock()
+ if conn.st == nil {
+ n := conn.cfg.Me
+ conn.st = state.NewTracker(n.Nick)
+ conn.st.NickInfo(n.Nick, n.Ident, n.Host, n.Name)
+ conn.cfg.Me = conn.st.Me()
+ conn.addSTHandlers()
+ }
+}
+
+// DisableStateTracking causes the client to stop tracking information
+// about the channels and nicks it knows of. It will also wipe current
+// state from the state tracker.
+func (conn *Conn) DisableStateTracking() {
+ conn.mu.Lock()
+ defer conn.mu.Unlock()
+ if conn.st != nil {
+ conn.cfg.Me = conn.st.Me()
+ conn.delSTHandlers()
+ conn.st.Wipe()
+ conn.st = nil
+ }
+}
+
+// Per-connection state initialisation.
+func (conn *Conn) initialise() {
+ conn.io = nil
+ conn.sock = nil
+ conn.in = make(chan *Line, 32)
+ conn.out = make(chan string, 32)
+ conn.die = make(chan struct{})
+ if conn.st != nil {
+ conn.st.Wipe()
+ }
+}
+
+// ConnectTo connects the IRC client to "host[:port]", which should be either
+// a hostname or an IP address, with an optional port. It sets the client's
+// Config.Server to host, Config.Pass to pass if one is provided, and then
+// calls Connect.
+func (conn *Conn) ConnectTo(host string, pass ...string) error {
+ conn.cfg.Server = host
+ if len(pass) > 0 {
+ conn.cfg.Pass = pass[0]
+ }
+ return conn.Connect()
+}
+
+// Connect connects the IRC client to the server configured in Config.Server.
+// To enable explicit SSL on the connection to the IRC server, set Config.SSL
+// to true before calling Connect(). The port will default to 6697 if SSL is
+// enabled, and 6667 otherwise.
+// To enable connecting via a proxy server, set Config.Proxy to the proxy URL
+// (example socks5://localhost:9000) before calling Connect().
+//
+// Upon successful connection, Connected will return true and a REGISTER event
+// will be fired. This is mostly for internal use; it is suggested that a
+// handler for the CONNECTED event is used to perform any initial client work
+// like joining channels and sending messages.
+func (conn *Conn) Connect() error {
+ // We don't want to hold conn.mu while firing the REGISTER event,
+ // and it's much easier and less error prone to defer the unlock,
+ // so the connect mechanics have been delegated to internalConnect.
+ err := conn.internalConnect()
+ if err == nil {
+ conn.dispatch(&Line{Cmd: REGISTER, Time: time.Now()})
+ }
+ return err
+}
+
+// internalConnect handles the work of actually connecting to the server.
+func (conn *Conn) internalConnect() error {
+ conn.mu.Lock()
+ defer conn.mu.Unlock()
+ conn.initialise()
+
+ if conn.cfg.Server == "" {
+ return fmt.Errorf("irc.Connect(): cfg.Server must be non-empty")
+ }
+ if conn.connected {
+ return fmt.Errorf("irc.Connect(): Cannot connect to %s, already connected.", conn.cfg.Server)
+ }
+
+ if !hasPort(conn.cfg.Server) {
+ if conn.cfg.SSL {
+ conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6697")
+ } else {
+ conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6667")
+ }
+ }
+
+ if conn.cfg.Proxy != "" {
+ proxyURL, err := url.Parse(conn.cfg.Proxy)
+ if err != nil {
+ return err
+ }
+ conn.proxyDialer, err = proxy.FromURL(proxyURL, conn.dialer)
+ if err != nil {
+ return err
+ }
+
+ logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server)
+ if s, err := conn.proxyDialer.Dial("tcp", conn.cfg.Server); err == nil {
+ conn.sock = s
+ } else {
+ return err
+ }
+ } else {
+ logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server)
+ if s, err := conn.dialer.Dial("tcp", conn.cfg.Server); err == nil {
+ conn.sock = s
+ } else {
+ return err
+ }
+ }
+
+ if conn.cfg.SSL {
+ logging.Info("irc.Connect(): Performing SSL handshake.")
+ s := tls.Client(conn.sock, conn.cfg.SSLConfig)
+ if err := s.Handshake(); err != nil {
+ return err
+ }
+ conn.sock = s
+ }
+
+ conn.postConnect(true)
+ conn.connected = true
+ return nil
+}
+
+// postConnect performs post-connection setup, for ease of testing.
+func (conn *Conn) postConnect(start bool) {
+ conn.io = bufio.NewReadWriter(
+ bufio.NewReader(conn.sock),
+ bufio.NewWriter(conn.sock))
+ if start {
+ conn.wg.Add(3)
+ go conn.send()
+ go conn.recv()
+ go conn.runLoop()
+ if conn.cfg.PingFreq > 0 {
+ conn.wg.Add(1)
+ go conn.ping()
+ }
+ }
+}
+
+// hasPort returns true if the string hostname has a :port suffix.
+// It was copied from net/http for great justice.
+func hasPort(s string) bool {
+ return strings.LastIndex(s, ":") > strings.LastIndex(s, "]")
+}
+
+// send is started as a goroutine after a connection is established.
+// It shuttles data from the output channel to write(), and is killed
+// when Conn.die is closed.
+func (conn *Conn) send() {
+ for {
+ select {
+ case line := <-conn.out:
+ if err := conn.write(line); err != nil {
+ logging.Error("irc.send(): %s", err.Error())
+ // We can't defer this, because Close() waits for it.
+ conn.wg.Done()
+ conn.Close()
+ return
+ }
+ case <-conn.die:
+ // control channel closed, bail out
+ conn.wg.Done()
+ return
+ }
+ }
+}
+
+// recv is started as a goroutine after a connection is established.
+// It receives "\r\n" terminated lines from the server, parses them into
+// Lines, and sends them to the input channel.
+func (conn *Conn) recv() {
+ for {
+ s, err := conn.io.ReadString('\n')
+ if err != nil {
+ if err != io.EOF {
+ logging.Error("irc.recv(): %s", err.Error())
+ }
+ // We can't defer this, because Close() waits for it.
+ conn.wg.Done()
+ conn.Close()
+ return
+ }
+ s = strings.Trim(s, "\r\n")
+ logging.Debug("<- %s", s)
+
+ if line := ParseLine(s); line != nil {
+ line.Time = time.Now()
+ conn.in <- line
+ } else {
+ logging.Warn("irc.recv(): problems parsing line:\n %s", s)
+ }
+ }
+}
+
+// ping is started as a goroutine after a connection is established, as
+// long as Config.PingFreq >0. It pings the server every PingFreq seconds.
+func (conn *Conn) ping() {
+ defer conn.wg.Done()
+ tick := time.NewTicker(conn.cfg.PingFreq)
+ for {
+ select {
+ case <-tick.C:
+ conn.Ping(fmt.Sprintf("%d", time.Now().UnixNano()))
+ case <-conn.die:
+ // control channel closed, bail out
+ tick.Stop()
+ return
+ }
+ }
+}
+
+// runLoop is started as a goroutine after a connection is established.
+// It pulls Lines from the input channel and dispatches them to any
+// handlers that have been registered for that IRC verb.
+func (conn *Conn) runLoop() {
+ defer conn.wg.Done()
+ for {
+ select {
+ case line := <-conn.in:
+ conn.dispatch(line)
+ case <-conn.die:
+ // control channel closed, bail out
+ return
+ }
+ }
+}
+
+// write writes a \r\n terminated line of output to the connected server,
+// using Hybrid's algorithm to rate limit if conn.cfg.Flood is false.
+func (conn *Conn) write(line string) error {
+ if !conn.cfg.Flood {
+ if t := conn.rateLimit(len(line)); t != 0 {
+ // sleep for the current line's time value before sending it
+ logging.Info("irc.rateLimit(): Flood! Sleeping for %.2f secs.",
+ t.Seconds())
+ <-time.After(t)
+ }
+ }
+
+ if _, err := conn.io.WriteString(line + "\r\n"); err != nil {
+ return err
+ }
+ if err := conn.io.Flush(); err != nil {
+ return err
+ }
+ if strings.HasPrefix(line, "PASS") {
+ line = "PASS **************"
+ }
+ logging.Debug("-> %s", line)
+ return nil
+}
+
+// rateLimit implements Hybrid's flood control algorithm for outgoing lines.
+func (conn *Conn) rateLimit(chars int) time.Duration {
+ // Hybrid's algorithm allows for 2 seconds per line and an additional
+ // 1/120 of a second per character on that line.
+ linetime := 2*time.Second + time.Duration(chars)*time.Second/120
+ elapsed := time.Now().Sub(conn.lastsent)
+ if conn.badness += linetime - elapsed; conn.badness < 0 {
+ // negative badness times are badness...
+ conn.badness = 0
+ }
+ conn.lastsent = time.Now()
+ // If we've sent more than 10 second's worth of lines according to the
+ // calculation above, then we're at risk of "Excess Flood".
+ if conn.badness > 10*time.Second {
+ return linetime
+ }
+ return 0
+}
+
+// Close tears down all connection-related state. It is called when either
+// the sending or receiving goroutines encounter an error.
+// It may also be used to forcibly shut down the connection to the server.
+func (conn *Conn) Close() error {
+ // Guard against double-call of Close() if we get an error in send()
+ // as calling sock.Close() will cause recv() to receive EOF in readstring()
+ conn.mu.Lock()
+ if !conn.connected {
+ conn.mu.Unlock()
+ return nil
+ }
+ logging.Info("irc.Close(): Disconnected from server.")
+ conn.connected = false
+ err := conn.sock.Close()
+ close(conn.die)
+ // Drain both in and out channels to avoid a deadlock if the buffers
+ // have filled. See TestSendDeadlockOnFullBuffer in connection_test.go.
+ conn.drainIn()
+ conn.drainOut()
+ conn.wg.Wait()
+ conn.mu.Unlock()
+ // Dispatch after closing connection but before reinit
+ // so event handlers can still access state information.
+ conn.dispatch(&Line{Cmd: DISCONNECTED, Time: time.Now()})
+ return err
+}
+
+// drainIn sends all data buffered in conn.in to /dev/null.
+func (conn *Conn) drainIn() {
+ for {
+ select {
+ case <-conn.in:
+ default:
+ return
+ }
+ }
+}
+
+// drainOut does the same for conn.out. Generics!
+func (conn *Conn) drainOut() {
+ for {
+ select {
+ case <-conn.out:
+ default:
+ return
+ }
+ }
+}
+
+// Dumps a load of information about the current state of the connection to a
+// string for debugging state tracking and other such things.
+func (conn *Conn) String() string {
+ str := "GoIRC Connection\n"
+ str += "----------------\n\n"
+ if conn.Connected() {
+ str += "Connected to " + conn.cfg.Server + "\n\n"
+ } else {
+ str += "Not currently connected!\n\n"
+ }
+ str += conn.Me().String() + "\n"
+ if conn.st != nil {
+ str += conn.st.String() + "\n"
+ }
+ return str
+}
diff --git a/vendor/github.com/fluffle/goirc/client/connection_test.go b/vendor/github.com/fluffle/goirc/client/connection_test.go
new file mode 100644
index 0000000..acf4713
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/connection_test.go
@@ -0,0 +1,585 @@
+package client
+
+import (
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/fluffle/goirc/state"
+ "github.com/golang/mock/gomock"
+)
+
+type checker struct {
+ t *testing.T
+ c chan struct{}
+}
+
+func callCheck(t *testing.T) checker {
+ return checker{t: t, c: make(chan struct{})}
+}
+
+func (c checker) call() {
+ c.c <- struct{}{}
+}
+
+func (c checker) assertNotCalled(fmt string, args ...interface{}) {
+ select {
+ case <-c.c:
+ c.t.Errorf(fmt, args...)
+ default:
+ }
+}
+
+func (c checker) assertWasCalled(fmt string, args ...interface{}) {
+ select {
+ case <-c.c:
+ case <-time.After(time.Millisecond):
+ // Usually need to wait for goroutines to settle :-/
+ c.t.Errorf(fmt, args...)
+ }
+}
+
+type testState struct {
+ ctrl *gomock.Controller
+ st *state.MockTracker
+ nc *mockNetConn
+ c *Conn
+}
+
+// NOTE: including a second argument at all prevents calling c.postConnect()
+func setUp(t *testing.T, start ...bool) (*Conn, *testState) {
+ ctrl := gomock.NewController(t)
+ st := state.NewMockTracker(ctrl)
+ nc := MockNetConn(t)
+ c := SimpleClient("test", "test", "Testing IRC")
+ c.initialise()
+
+ c.st = st
+ c.sock = nc
+ c.cfg.Flood = true // Tests can take a while otherwise
+ c.connected = true
+ // If a second argument is passed to setUp, we tell postConnect not to
+ // start the various goroutines that shuttle data around.
+ c.postConnect(len(start) == 0)
+ // Sleep 1ms to allow background routines to start.
+ <-time.After(time.Millisecond)
+
+ return c, &testState{ctrl, st, nc, c}
+}
+
+func (s *testState) tearDown() {
+ s.nc.ExpectNothing()
+ s.c.Close()
+ s.ctrl.Finish()
+}
+
+// Practically the same as the above test, but Close is called implicitly
+// by recv() getting an EOF from the mock connection.
+func TestEOF(t *testing.T) {
+ c, s := setUp(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 := callCheck(t)
+ c.HandleFunc(DISCONNECTED, func(conn *Conn, line *Line) {
+ dcon.call()
+ })
+
+ // Simulate EOF from server
+ s.nc.Close()
+
+ // Verify that disconnected handler was called
+ dcon.assertWasCalled("Conn did not call disconnected handlers.")
+
+ // Verify that the connection no longer thinks it's connected
+ if c.Connected() {
+ t.Errorf("Conn still thinks it's connected to the server.")
+ }
+}
+
+func TestClientAndStateTracking(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ st := state.NewMockTracker(ctrl)
+ c := SimpleClient("test", "test", "Testing IRC")
+
+ // Assert some basic things about the initial state of the Conn struct
+ me := c.cfg.Me
+ if me.Nick != "test" || me.Ident != "test" ||
+ me.Name != "Testing IRC" || me.Host != "" {
+ t.Errorf("Conn.cfg.Me not correctly initialised.")
+ }
+ // Check that the internal handlers are correctly set up
+ for k, _ := range intHandlers {
+ if _, ok := c.intHandlers.set[strings.ToLower(k)]; !ok {
+ t.Errorf("Missing internal handler for '%s'.", k)
+ }
+ }
+
+ // Now enable the state tracking code and check its handlers
+ c.EnableStateTracking()
+ for k, _ := range stHandlers {
+ if _, ok := c.intHandlers.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))
+ }
+ if neu := c.Me(); neu.Nick != me.Nick || neu.Ident != me.Ident ||
+ neu.Name != me.Name || neu.Host != me.Host {
+ t.Errorf("Enabling state tracking erased information about me!")
+ }
+
+ // We're expecting the untracked me to be replaced by a tracked one
+ if c.st == nil {
+ t.Errorf("State tracker not enabled correctly.")
+ }
+ if me = c.cfg.Me; me.Nick != "test" || me.Ident != "test" ||
+ me.Name != "Testing IRC" || me.Host != "" {
+ t.Errorf("Enabling state tracking did not replace Me correctly.")
+ }
+
+ // Now, shim in the mock state tracker and test disabling state tracking
+ c.st = st
+ gomock.InOrder(
+ st.EXPECT().Me().Return(me),
+ st.EXPECT().Wipe(),
+ )
+ c.DisableStateTracking()
+ if c.st != nil || !c.cfg.Me.Equals(me) {
+ t.Errorf("State tracker not disabled correctly.")
+ }
+
+ // Finally, check state tracking handlers were all removed correctly
+ for k, _ := range stHandlers {
+ if _, ok := c.intHandlers.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()
+}
+
+func TestSendExitsOnDie(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ defer s.tearDown()
+
+ // Assert that before send is running, nothing should be sent to the socket
+ // but writes to the buffered channel "out" should not block.
+ c.out <- "SENT BEFORE START"
+ s.nc.ExpectNothing()
+
+ // We want to test that the a goroutine calling send will exit correctly.
+ exited := callCheck(t)
+ // send() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(1)
+ go func() {
+ c.send()
+ exited.call()
+ }()
+
+ // send is now running in the background as if started by postConnect.
+ // This should read the line previously buffered in c.out, and write it
+ // to the socket connection.
+ s.nc.Expect("SENT BEFORE START")
+
+ // Send another line, just to be sure :-)
+ c.out <- "SENT AFTER START"
+ s.nc.Expect("SENT AFTER START")
+
+ // Now, use the control channel to exit send and kill the goroutine.
+ // This sneakily uses the fact that the other two goroutines that would
+ // normally be waiting for die to close are not running, so we only send
+ // to the goroutine started above. Normally Close() closes c.die and
+ // signals to all three goroutines (send, ping, runLoop) to exit.
+ exited.assertNotCalled("Exited before signal sent.")
+ c.die <- struct{}{}
+ exited.assertWasCalled("Didn't exit after signal.")
+ s.nc.ExpectNothing()
+
+ // Sending more on c.out shouldn't reach the network.
+ c.out <- "SENT AFTER END"
+ s.nc.ExpectNothing()
+}
+
+func TestSendExitsOnWriteError(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ // We can't use tearDown here because we're testing shutdown conditions
+ // (and so need to EXPECT() a call to st.Wipe() in the right place)
+ defer s.ctrl.Finish()
+
+ // We want to test that the a goroutine calling send will exit correctly.
+ exited := callCheck(t)
+ // send() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(1)
+ go func() {
+ c.send()
+ exited.call()
+ }()
+
+ // Send a line to be sure things are good.
+ c.out <- "SENT AFTER START"
+ s.nc.Expect("SENT AFTER START")
+
+ // Now, close the underlying socket to cause write() to return an error.
+ // This will call Close() => a call to st.Wipe() will happen.
+ exited.assertNotCalled("Exited before signal sent.")
+ s.nc.Close()
+ // Sending more on c.out shouldn't reach the network, but we need to send
+ // *something* to trigger a call to write() that will fail.
+ c.out <- "SENT AFTER END"
+ exited.assertWasCalled("Didn't exit after signal.")
+ s.nc.ExpectNothing()
+}
+
+func TestSendDeadlockOnFullBuffer(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ // We can't use tearDown here because we're testing a deadlock condition
+ // and if tearDown tries to call Close() it will deadlock some more
+ // because send() is holding the conn mutex via Close() already.
+ defer s.ctrl.Finish()
+
+ // We want to test that the a goroutine calling send will exit correctly.
+ loopExit := callCheck(t)
+ sendExit := callCheck(t)
+ // send() and runLoop() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(2)
+
+ // The deadlock arises when a handler being called from conn.dispatch() in
+ // runLoop() tries to write to conn.out to send a message back to the IRC
+ // server, but the buffer is full. If at the same time send() is
+ // calling conn.Close() and waiting in there for runLoop() to call
+ // conn.wg.Done(), it will not empty the buffer of conn.out => deadlock.
+ //
+ // We simulate this by artifically filling conn.out. We must use a
+ // goroutine to put in one more line than the buffer can hold, because
+ // send() will read a line from conn.out on its first loop iteration:
+ go func() {
+ for i := 0; i < 33; i++ {
+ c.out <- "FILL BUFFER WITH CRAP"
+ }
+ }()
+ // Then we add a handler that tries to write a line to conn.out:
+ c.HandleFunc(PRIVMSG, func(conn *Conn, line *Line) {
+ conn.Raw(line.Raw)
+ })
+ // And trigger it by starting runLoop and inserting a line into conn.in:
+ go func() {
+ c.runLoop()
+ loopExit.call()
+ }()
+ c.in <- &Line{Cmd: PRIVMSG, Raw: "WRITE THAT CAUSES DEADLOCK"}
+
+ // At this point the handler should be blocked on a write to conn.out,
+ // preventing runLoop from looping and thus noticing conn.die is closed.
+ //
+ // The next part is to force send() to call conn.Close(), which can
+ // be done by closing the fake net.Conn so that it returns an error on
+ // calls to Write():
+ s.nc.ExpectNothing()
+ s.nc.Close()
+
+ // Now when send is started it will read one line from conn.out and try
+ // to write it to the socket. It should immediately receive an error and
+ // call conn.Close(), triggering the deadlock as it waits forever for
+ // runLoop to call conn.wg.Done.
+ go func() {
+ c.send()
+ sendExit.call()
+ }()
+
+ // Make sure that things are definitely deadlocked.
+ <-time.After(time.Millisecond)
+
+ // Verify that the connection no longer thinks it's connected, i.e.
+ // conn.Close() has definitely been called. We can't call
+ // conn.Connected() here because conn.Close() holds the mutex.
+ if c.connected {
+ t.Errorf("Conn still thinks it's connected to the server.")
+ }
+
+ // We expect both loops to terminate cleanly. If either of them don't
+ // then we have successfully deadlocked :-(
+ loopExit.assertWasCalled("runLoop did not exit cleanly.")
+ sendExit.assertWasCalled("send did not exit cleanly.")
+}
+
+func TestRecv(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ // We can't use tearDown here because we're testing shutdown conditions
+ // (and so need to EXPECT() a call to st.Wipe() in the right place)
+ defer s.ctrl.Finish()
+
+ // Send a line before recv is started up, to verify nothing appears on c.in
+ s.nc.Send(":irc.server.org 001 test :First test line.")
+
+ // reader is a helper to do a "non-blocking" read of c.in
+ reader := func() *Line {
+ select {
+ case <-time.After(time.Millisecond):
+ case l := <-c.in:
+ return l
+ }
+ return nil
+ }
+ if l := reader(); l != nil {
+ t.Errorf("Line parsed before recv started.")
+ }
+
+ // We want to test that the a goroutine calling recv will exit correctly.
+ exited := callCheck(t)
+ // recv() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(1)
+ go func() {
+ c.recv()
+ exited.call()
+ }()
+
+ // Now, this should mean that we'll receive our parsed line on c.in
+ if l := reader(); l == nil || l.Cmd != "001" {
+ t.Errorf("Bad first line received on input channel")
+ }
+
+ // Send a second line, just to be sure.
+ s.nc.Send(":irc.server.org 002 test :Second test line.")
+ if l := reader(); l == nil || l.Cmd != "002" {
+ t.Errorf("Bad second line received on input channel.")
+ }
+
+ // Test that recv does something useful with a line it can't parse
+ // (not that there are many, ParseLine is forgiving).
+ s.nc.Send(":textwithnospaces")
+ if l := reader(); l != nil {
+ t.Errorf("Bad line still caused receive on input channel.")
+ }
+
+ // The only way recv() exits is when the socket closes.
+ exited.assertNotCalled("Exited before socket close.")
+ s.nc.Close()
+ exited.assertWasCalled("Didn't exit on socket close.")
+
+ // Since s.nc is closed we can't attempt another send on it...
+ if l := reader(); l != nil {
+ t.Errorf("Line received on input channel after socket close.")
+ }
+}
+
+func TestPing(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ defer s.tearDown()
+
+ res := time.Millisecond
+
+ // Windows has a timer resolution of 15.625ms by default.
+ // This means the test will be slower on windows, but
+ // should at least stop most of the flakiness...
+ // https://github.com/fluffle/goirc/issues/88
+ if runtime.GOOS == "windows" {
+ res = 15625 * time.Microsecond
+ }
+
+ // Set a low ping frequency for testing.
+ c.cfg.PingFreq = 10 * res
+
+ // reader is a helper to do a "non-blocking" read of c.out
+ reader := func() string {
+ select {
+ case <-time.After(res):
+ case s := <-c.out:
+ return s
+ }
+ return ""
+ }
+ if s := reader(); s != "" {
+ t.Errorf("Line output before ping started.")
+ }
+
+ // Start ping loop.
+ exited := callCheck(t)
+ // ping() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(1)
+ go func() {
+ c.ping()
+ exited.call()
+ }()
+
+ // The first ping should be after 10*res ms,
+ // so we don't expect anything now on c.in
+ if s := reader(); s != "" {
+ t.Errorf("Line output directly after ping started.")
+ }
+
+ <-time.After(c.cfg.PingFreq)
+ if s := reader(); s == "" || !strings.HasPrefix(s, "PING :") {
+ t.Errorf("Line not output after %s.", c.cfg.PingFreq)
+ }
+
+ // Reader waits for res ms and we call it a few times above.
+ <-time.After(7 * res)
+ if s := reader(); s != "" {
+ t.Errorf("Line output <%s after last ping.", 7*res)
+ }
+
+ // This is a short window in which the ping should happen
+ // This may result in flaky tests; sorry (and file a bug) if so.
+ <-time.After(2 * res)
+ if s := reader(); s == "" || !strings.HasPrefix(s, "PING :") {
+ t.Errorf("Line not output after another %s.", 2*res)
+ }
+
+ // Now kill the ping loop.
+ // This sneakily uses the fact that the other two goroutines that would
+ // normally be waiting for die to close are not running, so we only send
+ // to the goroutine started above. Normally Close() closes c.die and
+ // signals to all three goroutines (send, ping, runLoop) to exit.
+ exited.assertNotCalled("Exited before signal sent.")
+ c.die <- struct{}{}
+ exited.assertWasCalled("Didn't exit after signal.")
+ // Make sure we're no longer pinging by waiting >2x PingFreq
+ <-time.After(2*c.cfg.PingFreq + res)
+ if s := reader(); s != "" {
+ t.Errorf("Line output after ping stopped.")
+ }
+}
+
+func TestRunLoop(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ defer s.tearDown()
+
+ // Set up a handler to detect whether 001 handler is called
+ h001 := callCheck(t)
+ c.HandleFunc("001", func(conn *Conn, line *Line) {
+ h001.call()
+ })
+ h002 := callCheck(t)
+ // Set up a handler to detect whether 002 handler is called
+ c.HandleFunc("002", func(conn *Conn, line *Line) {
+ h002.call()
+ })
+
+ l1 := ParseLine(":irc.server.org 001 test :First test line.")
+ c.in <- l1
+ h001.assertNotCalled("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.
+ exited := callCheck(t)
+ // runLoop() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(1)
+ go func() {
+ c.runLoop()
+ exited.call()
+ }()
+ h001.assertWasCalled("001 handler not called after runLoop started.")
+
+ // Send another line, just to be sure :-)
+ h002.assertNotCalled("002 handler called before expected.")
+ l2 := ParseLine(":irc.server.org 002 test :Second test line.")
+ c.in <- l2
+ h002.assertWasCalled("002 handler not called while runLoop started.")
+
+ // Now, use the control channel to exit send and kill the goroutine.
+ // This sneakily uses the fact that the other two goroutines that would
+ // normally be waiting for die to close are not running, so we only send
+ // to the goroutine started above. Normally Close() closes c.die and
+ // signals to all three goroutines (send, ping, runLoop) to exit.
+ exited.assertNotCalled("Exited before signal sent.")
+ c.die <- struct{}{}
+ exited.assertWasCalled("Didn't exit after signal.")
+
+ // Sending more on c.in shouldn't dispatch any further events
+ c.in <- l1
+ h001.assertNotCalled("001 handler called after runLoop ended.")
+}
+
+func TestWrite(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ // We can't use tearDown here because we're testing shutdown conditions
+ // (and so need to EXPECT() a call to st.Wipe() in the right place)
+ defer s.ctrl.Finish()
+
+ // Write should just write a line to the socket.
+ if err := c.write("yo momma"); err != nil {
+ t.Errorf("Write returned unexpected error %v", err)
+ }
+ s.nc.Expect("yo momma")
+
+ // Flood control is disabled -- setUp sets c.cfg.Flood = true -- so we should
+ // not have set c.badness at this point.
+ if c.badness != 0 {
+ t.Errorf("Flood control used when Flood = true.")
+ }
+
+ c.cfg.Flood = false
+ if err := c.write("she so useless"); err != nil {
+ t.Errorf("Write returned unexpected error %v", err)
+ }
+ s.nc.Expect("she so useless")
+
+ // The lastsent time should have been updated very recently...
+ if time.Now().Sub(c.lastsent) > time.Millisecond {
+ t.Errorf("Flood control not used when Flood = false.")
+ }
+
+ // Finally, test the error state by closing the socket then writing.
+ s.nc.Close()
+ if err := c.write("she can't pass unit tests"); err == nil {
+ t.Errorf("Expected write to return error after socket close.")
+ }
+}
+
+func TestRateLimit(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ if c.badness != 0 {
+ t.Errorf("Bad initial values for rate limit variables.")
+ }
+
+ // We'll be needing this later...
+ abs := func(i time.Duration) time.Duration {
+ if i < 0 {
+ return -i
+ }
+ return i
+ }
+
+ // Since the changes to the time module, c.lastsent is now a time.Time.
+ // It's initialised on client creation to time.Now() which for the purposes
+ // of this test was probably around 1.2 ms ago. This is inconvenient.
+ // Making it >10s ago effectively clears out the inconsistency, as this
+ // makes elapsed > linetime and thus zeros c.badness and resets c.lastsent.
+ c.lastsent = time.Now().Add(-10 * time.Second)
+ if l := c.rateLimit(60); l != 0 || c.badness != 0 {
+ t.Errorf("Rate limit got non-zero badness from long-ago lastsent.")
+ }
+
+ // So, time at the nanosecond resolution is a bit of a bitch. Choosing 60
+ // 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 20us as a fuzz.
+ if l := c.rateLimit(60); l != 0 ||
+ abs(c.badness-2500*time.Millisecond) > 20*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-10500*time.Millisecond) > 20*time.Microsecond {
+ t.Errorf("Rate limit failed to return correct limiting values.")
+ t.Errorf("l=%d, badness=%d", l, c.badness)
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/dispatch.go b/vendor/github.com/fluffle/goirc/client/dispatch.go
new file mode 100644
index 0000000..4a5deb9
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/dispatch.go
@@ -0,0 +1,202 @@
+package client
+
+import (
+ "runtime"
+ "strings"
+ "sync"
+
+ "github.com/fluffle/goirc/logging"
+)
+
+// 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).
+//
+// Foreground handlers have a guarantee of protocol consistency: all the
+// handlers for one event will have finished before the handlers for the
+// next start processing. They are run in parallel but block the event
+// loop, so care should be taken to ensure these handlers are quick :-)
+//
+// Background handlers are run in parallel and do not block the event loop.
+// This is useful for things that may need to do significant work.
+type Handler interface {
+ Handle(*Conn, *Line)
+}
+
+// Removers allow for a handler that has been previously added to the client
+// to be removed.
+type Remover interface {
+ Remove()
+}
+
+// HandlerFunc allows a bare function with this signature to implement the
+// Handler interface. It is used by Conn.HandleFunc.
+type HandlerFunc func(*Conn, *Line)
+
+func (hf HandlerFunc) Handle(conn *Conn, line *Line) {
+ hf(conn, line)
+}
+
+// Handlers are organised using a map of linked-lists, with each map
+// key representing an IRC verb or numeric, and the linked list values
+// being handlers that are executed in parallel when a Line from the
+// server with that verb or numeric arrives.
+type hSet struct {
+ set map[string]*hList
+ sync.RWMutex
+}
+
+type hList struct {
+ start, end *hNode
+}
+
+// Storing the forward and backward links in the node allows O(1) removal.
+// This probably isn't strictly necessary but I think it's kinda nice.
+type hNode struct {
+ next, prev *hNode
+ set *hSet
+ event string
+ handler Handler
+}
+
+// A hNode implements both Handler (with configurable panic recovery)...
+func (hn *hNode) Handle(conn *Conn, line *Line) {
+ defer conn.cfg.Recover(conn, line)
+ hn.handler.Handle(conn, line)
+}
+
+// ... and Remover.
+func (hn *hNode) Remove() {
+ hn.set.remove(hn)
+}
+
+func handlerSet() *hSet {
+ return &hSet{set: make(map[string]*hList)}
+}
+
+// When a new Handler is added for an event, it is wrapped in a hNode and
+// returned as a Remover so the caller can remove it at a later time.
+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) getHandlers(ev string) []*hNode {
+ hs.RLock()
+ defer hs.RUnlock()
+ list, ok := hs.set[ev]
+ if !ok {
+ return nil
+ }
+ // Copy current list of handlers to a temporary slice under the lock.
+ handlers := make([]*hNode, 0)
+ for hn := list.start; hn != nil; hn = hn.next {
+ handlers = append(handlers, hn)
+ }
+ return handlers
+}
+
+func (hs *hSet) dispatch(conn *Conn, line *Line) {
+ ev := strings.ToLower(line.Cmd)
+ wg := &sync.WaitGroup{}
+ for _, hn := range hs.getHandlers(ev) {
+ wg.Add(1)
+ go func(hn *hNode) {
+ hn.Handle(conn, line.Copy())
+ wg.Done()
+ }(hn)
+ }
+ wg.Wait()
+}
+
+// Handle adds the provided handler to the foreground set for the named event.
+// It will return a Remover that allows that handler to be removed again.
+func (conn *Conn) Handle(name string, h Handler) Remover {
+ return conn.fgHandlers.add(name, h)
+}
+
+// HandleBG adds the provided handler to the background set for the named
+// event. It may go away in the future.
+// It will return a Remover that allows that handler to be removed again.
+func (conn *Conn) HandleBG(name string, h Handler) Remover {
+ return conn.bgHandlers.add(name, h)
+}
+
+func (conn *Conn) handle(name string, h Handler) Remover {
+ return conn.intHandlers.add(name, h)
+}
+
+// HandleFunc adds the provided function as a handler in the foreground set
+// for the named event.
+// It will return a Remover that allows that handler to be removed again.
+func (conn *Conn) HandleFunc(name string, hf HandlerFunc) Remover {
+ return conn.Handle(name, hf)
+}
+
+func (conn *Conn) dispatch(line *Line) {
+ // We run the internal handlers first, including all state tracking ones.
+ // This ensures that user-supplied handlers that use the tracker have a
+ // consistent view of the connection state in handlers that mutate it.
+ conn.intHandlers.dispatch(conn, line)
+ go conn.bgHandlers.dispatch(conn, line)
+ conn.fgHandlers.dispatch(conn, line)
+}
+
+// LogPanic is used as the default panic catcher for the client. If, like me,
+// you are not good with computer, and you'd prefer your bot not to vanish into
+// the ether whenever you make unfortunate programming mistakes, you may find
+// this useful: it will recover panics from handler code and log the errors.
+func (conn *Conn) LogPanic(line *Line) {
+ if err := recover(); err != nil {
+ _, f, l, _ := runtime.Caller(2)
+ logging.Error("%s:%d: panic: %v", f, l, err)
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/dispatch_test.go b/vendor/github.com/fluffle/goirc/client/dispatch_test.go
new file mode 100644
index 0000000..b79df64
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/dispatch_test.go
@@ -0,0 +1,201 @@
+package client
+
+import (
+ "sync/atomic"
+ "testing"
+ "time"
+)
+
+func TestHandlerSet(t *testing.T) {
+ // A Conn is needed here because the previous behaviour of passing nil to
+ // hset.dispatch causes a nil pointer dereference with panic recovery.
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ hs := handlerSet()
+ if len(hs.set) != 0 {
+ t.Errorf("New set contains things!")
+ }
+
+ callcount := new(int32)
+ f := func(_ *Conn, _ *Line) {
+ atomic.AddInt32(callcount, 1)
+ }
+
+ // 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 atomic.LoadInt32(callcount) != 0 {
+ t.Errorf("Something incremented call count before we were expecting it.")
+ }
+ hs.dispatch(c, &Line{Cmd: "One"})
+ <-time.After(time.Millisecond)
+ if atomic.LoadInt32(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(c, &Line{Cmd: "One"})
+ <-time.After(time.Millisecond)
+ if atomic.LoadInt32(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(c, &Line{Cmd: "One"})
+ <-time.After(time.Millisecond)
+ if atomic.LoadInt32(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(c, &Line{Cmd: "One"})
+ <-time.After(time.Millisecond)
+ if atomic.LoadInt32(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(c, &Line{Cmd: "One"})
+ <-time.After(time.Millisecond)
+ if atomic.LoadInt32(callcount) != 10 {
+ t.Errorf("Our handler was called?")
+ }
+}
+
+func TestPanicRecovery(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ recovered := callCheck(t)
+ c.cfg.Recover = func(conn *Conn, line *Line) {
+ if err, ok := recover().(string); ok && err == "panic!" {
+ recovered.call()
+ }
+ }
+ c.HandleFunc(PRIVMSG, func(conn *Conn, line *Line) {
+ panic("panic!")
+ })
+ c.in <- ParseLine(":nick!user@host.com PRIVMSG #channel :OH NO PIGEONS")
+ recovered.assertWasCalled("Failed to recover panic!")
+}
diff --git a/vendor/github.com/fluffle/goirc/client/doc.go b/vendor/github.com/fluffle/goirc/client/doc.go
new file mode 100644
index 0000000..08a79dd
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/doc.go
@@ -0,0 +1,34 @@
+// Package client implements an IRC client. It handles protocol basics
+// such as initial connection and responding to server PINGs, and has
+// optional state tracking support which will keep tabs on every nick
+// present in the same channels as the client. Other features include
+// SSL support, automatic splitting of long lines, and panic recovery
+// for handlers.
+//
+// Incoming IRC messages are parsed into client.Line structs and trigger
+// events based on the IRC verb (e.g. PRIVMSG) of the message. Handlers
+// for these events conform to the client.Handler interface; a HandlerFunc
+// type to wrap bare functions is provided a-la the net/http package.
+//
+// Creating a client, adding a handler and connecting to a server looks
+// soemthing like this, for the simple case:
+//
+// // Create a new client, which will connect with the nick "myNick"
+// irc := client.SimpleClient("myNick")
+//
+// // Add a handler that waits for the "disconnected" event and
+// // closes a channel to signal everything is done.
+// disconnected := make(chan struct{})
+// c.HandleFunc("disconnected", func(c *client.Conn, l *client.Line) {
+// close(disconnected)
+// })
+//
+// // Connect to an IRC server.
+// if err := c.ConnectTo("irc.freenode.net"); err != nil {
+// log.Fatalf("Connection error: %v\n", err)
+// }
+//
+// // Wait for disconnection.
+// <-disconnected
+//
+package client
diff --git a/vendor/github.com/fluffle/goirc/client/handlers.go b/vendor/github.com/fluffle/goirc/client/handlers.go
new file mode 100644
index 0000000..b538579
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/handlers.go
@@ -0,0 +1,105 @@
+package client
+
+// this file contains the basic set of event handlers
+// to manage tracking an irc connection etc.
+
+import (
+ "strings"
+ "time"
+)
+
+// sets up the internal event handlers to do essential IRC protocol things
+var intHandlers = map[string]HandlerFunc{
+ REGISTER: (*Conn).h_REGISTER,
+ "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 {
+ // internal handlers are essential for the IRC client
+ // to function, so we don't save their Removers here
+ conn.handle(n, h)
+ }
+}
+
+// Basic ping/pong handler
+func (conn *Conn) h_PING(line *Line) {
+ conn.Pong(line.Args[0])
+}
+
+// Handler for initial registration with server once tcp connection is made.
+func (conn *Conn) h_REGISTER(line *Line) {
+ if conn.cfg.Pass != "" {
+ conn.Pass(conn.cfg.Pass)
+ }
+ conn.Nick(conn.cfg.Me.Nick)
+ conn.User(conn.cfg.Me.Ident, conn.cfg.Me.Name)
+}
+
+// Handler to trigger a CONNECTED event on receipt of numeric 001
+func (conn *Conn) h_001(line *Line) {
+ // we're connected!
+ conn.dispatch(&Line{Cmd: CONNECTED, Time: time.Now()})
+ // 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 {
+ t = t[idx+1:]
+ if idx = strings.Index(t, "@"); idx != -1 {
+ if conn.st != nil {
+ me := conn.Me()
+ conn.st.NickInfo(me.Nick, me.Ident, t[idx+1:], me.Name)
+ } else {
+ conn.cfg.Me.Host = t[idx+1:]
+ }
+ }
+ }
+}
+
+// XXX: do we need 005 protocol support message parsing here?
+// probably in the future, but I can't quite be arsed yet.
+/*
+ :irc.pl0rt.org 005 GoTest CMDS=KNOCK,MAP,DCCALLOW,USERIP UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=20 CHANLIMIT=#:20 MAXLIST=b:60,e:60,I:60 NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 :are supported by this server
+ :irc.pl0rt.org 005 GoTest MAXTARGETS=20 WALLCHOPS WATCH=128 WATCHOPTS=A SILENCE=15 MODES=12 CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beI,kfL,lj,psmntirRcOAQKVCuzNSMT NETWORK=bb101.net CASEMAPPING=ascii EXTBAN=~,cqnr ELIST=MNUCT :are supported by this server
+ :irc.pl0rt.org 005 GoTest STATUSMSG=~&@%+ EXCEPTS INVEX :are supported by this server
+*/
+
+// Handler to deal with "433 :Nickname already in use"
+func (conn *Conn) h_433(line *Line) {
+ // Args[1] is the new nick we were attempting to acquire
+ me := conn.Me()
+ neu := conn.cfg.NewNick(line.Args[1])
+ conn.Nick(neu)
+ if !line.argslen(1) {
+ return
+ }
+ // if this is happening before we're properly connected (i.e. the nick
+ // we sent in the initial NICK command is in use) we will not receive
+ // a NICK message to confirm our change of nick, so ReNick here...
+ if line.Args[1] == me.Nick {
+ if conn.st != nil {
+ conn.cfg.Me = conn.st.ReNick(me.Nick, neu)
+ } else {
+ conn.cfg.Me.Nick = neu
+ }
+ }
+}
+
+// Handle VERSION requests and CTCP PING
+func (conn *Conn) h_CTCP(line *Line) {
+ if line.Args[0] == VERSION {
+ conn.CtcpReply(line.Nick, VERSION, conn.cfg.Version)
+ } else if line.Args[0] == PING && line.argslen(2) {
+ conn.CtcpReply(line.Nick, PING, line.Args[2])
+ }
+}
+
+// Handle updating our own NICK if we're not using the state tracker
+func (conn *Conn) h_NICK(line *Line) {
+ if conn.st == nil && line.Nick == conn.cfg.Me.Nick {
+ conn.cfg.Me.Nick = line.Args[0]
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/handlers_test.go b/vendor/github.com/fluffle/goirc/client/handlers_test.go
new file mode 100644
index 0000000..7808022
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/handlers_test.go
@@ -0,0 +1,451 @@
+package client
+
+import (
+ "github.com/fluffle/goirc/state"
+ "github.com/golang/mock/gomock"
+ "testing"
+ "time"
+)
+
+// This test performs a simple end-to-end verification of correct line parsing
+// and event dispatch as well as testing the PING handler. All the other tests
+// in this file will call their respective handlers synchronously, otherwise
+// testing becomes more difficult.
+func TestPING(t *testing.T) {
+ _, s := setUp(t)
+ defer s.tearDown()
+ s.nc.Send("PING :1234567890")
+ s.nc.Expect("PONG :1234567890")
+}
+
+// Test the REGISTER handler matches section 3.1 of rfc2812
+func TestREGISTER(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ c.h_REGISTER(&Line{Cmd: REGISTER})
+ s.nc.Expect("NICK test")
+ s.nc.Expect("USER test 12 * :Testing IRC")
+ s.nc.ExpectNothing()
+
+ c.cfg.Pass = "12345"
+ c.cfg.Me.Ident = "idiot"
+ c.cfg.Me.Name = "I've got the same combination on my luggage!"
+ c.h_REGISTER(&Line{Cmd: REGISTER})
+ s.nc.Expect("PASS 12345")
+ s.nc.Expect("NICK test")
+ s.nc.Expect("USER idiot 12 * :I've got the same combination on my luggage!")
+ s.nc.ExpectNothing()
+}
+
+// Test the handler for 001 / RPL_WELCOME
+func Test001(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ 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
+ hcon := false
+ c.HandleFunc("connected", func(conn *Conn, line *Line) {
+ hcon = true
+ })
+
+ // Test state tracking first.
+ gomock.InOrder(
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NickInfo("test", "test", "somehost.com", "Testing IRC"),
+ )
+ // 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.")
+ }
+
+ // Now without state tracking.
+ c.st = nil
+ c.h_001(l)
+ // Check host parsed correctly
+ if c.cfg.Me.Host != "somehost.com" {
+ t.Errorf("Host parsing failed, host is '%s'.", c.cfg.Me.Host)
+ }
+ c.st = s.st
+}
+
+// Test the handler for 433 / ERR_NICKNAMEINUSE
+func Test433(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Call handler with a 433 line, not triggering c.cfg.Me.Renick()
+ s.st.EXPECT().Me().Return(c.cfg.Me)
+ c.h_433(ParseLine(":irc.server.org 433 test new :Nickname is already in use."))
+ s.nc.Expect("NICK new_")
+
+ // Send a line that will trigger a renick. This happens when our wanted
+ // nick is unavailable during initial negotiation, so we must choose a
+ // different one before the connection can proceed. No NICK line will be
+ // sent by the server to confirm nick change in this case.
+ gomock.InOrder(
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().ReNick("test", "test_").Return(c.cfg.Me),
+ )
+ c.h_433(ParseLine(":irc.server.org 433 test test :Nickname is already in use."))
+ s.nc.Expect("NICK test_")
+
+ // Test the code path that *doesn't* involve state tracking.
+ c.st = nil
+ c.h_433(ParseLine(":irc.server.org 433 test test :Nickname is already in use."))
+ s.nc.Expect("NICK test_")
+
+ if c.cfg.Me.Nick != "test_" {
+ t.Errorf("My nick not updated from '%s'.", c.cfg.Me.Nick)
+ }
+ c.st = s.st
+}
+
+// Test the handler for NICK messages when state tracking is disabled
+func TestNICK(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // State tracking is enabled by default in setUp
+ c.st = nil
+
+ // Call handler with a NICK line changing "our" nick to test1.
+ c.h_NICK(ParseLine(":test!test@somehost.com NICK :test1"))
+
+ // Verify that our Nick has changed
+ if c.cfg.Me.Nick != "test1" {
+ t.Errorf("NICK did not result in changing our nick.")
+ }
+
+ // Send a NICK line for something that isn't us.
+ c.h_NICK(ParseLine(":blah!moo@cows.com NICK :milk"))
+
+ // Verify that our Nick hasn't changed
+ if c.cfg.Me.Nick != "test1" {
+ t.Errorf("NICK did not result in changing our nick.")
+ }
+
+ // Re-enable state tracking and send a line that *should* change nick.
+ c.st = s.st
+ c.h_NICK(ParseLine(":test1!test@somehost.com NICK :test2"))
+
+ // Verify that our Nick hasn't changed (should be handled by h_STNICK).
+ if c.cfg.Me.Nick != "test1" {
+ t.Errorf("NICK changed our nick when state tracking enabled.")
+ }
+}
+
+// Test the handler for CTCP messages
+func TestCTCP(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Call handler with CTCP VERSION
+ c.h_CTCP(ParseLine(":blah!moo@cows.com PRIVMSG test :\001VERSION\001"))
+
+ // Expect a version reply
+ s.nc.Expect("NOTICE blah :\001VERSION Powered by GoIRC\001")
+
+ // Call handler with CTCP PING
+ c.h_CTCP(ParseLine(":blah!moo@cows.com PRIVMSG test :\001PING 1234567890\001"))
+
+ // Expect a ping reply
+ s.nc.Expect("NOTICE blah :\001PING 1234567890\001")
+
+ // Call handler with CTCP UNKNOWN
+ c.h_CTCP(ParseLine(":blah!moo@cows.com PRIVMSG test :\001UNKNOWN ctcp\001"))
+}
+
+// Test the handler for JOIN messages
+func TestJOIN(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // The state tracker should be creating a new channel in this first test
+ chan1 := &state.Channel{Name: "#test1"}
+
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(nil),
+ s.st.EXPECT().GetNick("test").Return(c.cfg.Me),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NewChannel("#test1").Return(chan1),
+ s.st.EXPECT().Associate("#test1", "test"),
+ )
+
+ // Use #test1 to test expected behaviour
+ // Call handler with JOIN by test to #test1
+ c.h_JOIN(ParseLine(":test!test@somehost.com JOIN :#test1"))
+
+ // Verify that the MODE and WHO commands are sent correctly
+ s.nc.Expect("MODE #test1")
+ s.nc.Expect("WHO #test1")
+
+ // In this second test, we should be creating a new nick
+ nick1 := &state.Nick{Nick: "user1"}
+
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(chan1),
+ s.st.EXPECT().GetNick("user1").Return(nil),
+ s.st.EXPECT().NewNick("user1").Return(nick1),
+ s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "").Return(nick1),
+ s.st.EXPECT().Associate("#test1", "user1"),
+ )
+
+ // OK, now #test1 exists, JOIN another user we don't know about
+ c.h_JOIN(ParseLine(":user1!ident1@host1.com JOIN :#test1"))
+
+ // Verify that the WHO command is sent correctly
+ s.nc.Expect("WHO user1")
+
+ // In this third test, we'll be pretending we know about the nick already.
+ nick2 := &state.Nick{Nick: "user2"}
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(chan1),
+ s.st.EXPECT().GetNick("user2").Return(nick2),
+ s.st.EXPECT().Associate("#test1", "user2"),
+ )
+ c.h_JOIN(ParseLine(":user2!ident2@host2.com JOIN :#test1"))
+
+ // Test error paths
+ gomock.InOrder(
+ // unknown channel, unknown nick
+ s.st.EXPECT().GetChannel("#test2").Return(nil),
+ s.st.EXPECT().GetNick("blah").Return(nil),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ // unknown channel, known nick that isn't Me.
+ s.st.EXPECT().GetChannel("#test2").Return(nil),
+ s.st.EXPECT().GetNick("user2").Return(nick2),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ )
+ c.h_JOIN(ParseLine(":blah!moo@cows.com JOIN :#test2"))
+ c.h_JOIN(ParseLine(":user2!ident2@host2.com JOIN :#test2"))
+}
+
+// Test the handler for PART messages
+func TestPART(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // PART should dissociate a nick from a channel.
+ s.st.EXPECT().Dissociate("#test1", "user1")
+ c.h_PART(ParseLine(":user1!ident1@host1.com PART #test1 :Bye!"))
+}
+
+// Test the handler for KICK messages
+// (this is very similar to the PART message test)
+func TestKICK(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // KICK should dissociate a nick from a channel.
+ s.st.EXPECT().Dissociate("#test1", "user1")
+ c.h_KICK(ParseLine(":test!test@somehost.com KICK #test1 user1 :Bye!"))
+}
+
+// Test the handler for QUIT messages
+func TestQUIT(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Have user1 QUIT. All possible errors handled by state tracker \o/
+ s.st.EXPECT().DelNick("user1")
+ c.h_QUIT(ParseLine(":user1!ident1@host1.com QUIT :Bye!"))
+}
+
+// Test the handler for MODE messages
+func TestMODE(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Channel modes
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}),
+ s.st.EXPECT().ChannelModes("#test1", "+sk", "somekey"),
+ )
+ c.h_MODE(ParseLine(":user1!ident1@host1.com MODE #test1 +sk somekey"))
+
+ // Nick modes for Me.
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("test").Return(nil),
+ s.st.EXPECT().GetNick("test").Return(c.cfg.Me),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NickModes("test", "+i"),
+ )
+ c.h_MODE(ParseLine(":test!test@somehost.com MODE test +i"))
+
+ // Check error paths
+ gomock.InOrder(
+ // send a valid user mode that's not us
+ s.st.EXPECT().GetChannel("user1").Return(nil),
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ // Send a random mode for an unknown channel
+ s.st.EXPECT().GetChannel("#test2").Return(nil),
+ s.st.EXPECT().GetNick("#test2").Return(nil),
+ )
+ c.h_MODE(ParseLine(":user1!ident1@host1.com MODE user1 +w"))
+ c.h_MODE(ParseLine(":user1!ident1@host1.com MODE #test2 +is"))
+}
+
+// Test the handler for TOPIC messages
+func TestTOPIC(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure TOPIC reply calls Topic
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}),
+ s.st.EXPECT().Topic("#test1", "something something"),
+ )
+ c.h_TOPIC(ParseLine(":user1!ident1@host1.com TOPIC #test1 :something something"))
+
+ // Check error paths -- send a topic for an unknown channel
+ s.st.EXPECT().GetChannel("#test2").Return(nil)
+ c.h_TOPIC(ParseLine(":user1!ident1@host1.com TOPIC #test2 :dark side"))
+}
+
+// Test the handler for 311 / RPL_WHOISUSER
+func Test311(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure 311 reply calls NickInfo
+ gomock.InOrder(
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "name"),
+ )
+ c.h_311(ParseLine(":irc.server.org 311 test user1 ident1 host1.com * :name"))
+
+ // Check error paths -- send a 311 for an unknown nick
+ s.st.EXPECT().GetNick("user2").Return(nil)
+ c.h_311(ParseLine(":irc.server.org 311 test user2 ident2 host2.com * :dongs"))
+}
+
+// Test the handler for 324 / RPL_CHANNELMODEIS
+func Test324(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure 324 reply calls ChannelModes
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}),
+ s.st.EXPECT().ChannelModes("#test1", "+sk", "somekey"),
+ )
+ c.h_324(ParseLine(":irc.server.org 324 test #test1 +sk somekey"))
+
+ // Check error paths -- send 324 for an unknown channel
+ s.st.EXPECT().GetChannel("#test2").Return(nil)
+ c.h_324(ParseLine(":irc.server.org 324 test #test2 +pmt"))
+}
+
+// Test the handler for 332 / RPL_TOPIC
+func Test332(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure 332 reply calls Topic
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}),
+ s.st.EXPECT().Topic("#test1", "something something"),
+ )
+ c.h_332(ParseLine(":irc.server.org 332 test #test1 :something something"))
+
+ // Check error paths -- send 332 for an unknown channel
+ s.st.EXPECT().GetChannel("#test2").Return(nil)
+ c.h_332(ParseLine(":irc.server.org 332 test #test2 :dark side"))
+}
+
+// Test the handler for 352 / RPL_WHOREPLY
+func Test352(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure 352 reply calls NickInfo and NickModes
+ gomock.InOrder(
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "name"),
+ )
+ c.h_352(ParseLine(":irc.server.org 352 test #test1 ident1 host1.com irc.server.org user1 G :0 name"))
+
+ // Check that modes are set correctly from WHOREPLY
+ gomock.InOrder(
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "name"),
+ s.st.EXPECT().NickModes("user1", "+o"),
+ s.st.EXPECT().NickModes("user1", "+i"),
+ )
+ c.h_352(ParseLine(":irc.server.org 352 test #test1 ident1 host1.com irc.server.org user1 H* :0 name"))
+
+ // Check error paths -- send a 352 for an unknown nick
+ s.st.EXPECT().GetNick("user2").Return(nil)
+ c.h_352(ParseLine(":irc.server.org 352 test #test2 ident2 host2.com irc.server.org user2 G :0 fooo"))
+}
+
+// Test the handler for 353 / RPL_NAMREPLY
+func Test353(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // 353 handler is called twice, so GetChannel will be called twice
+ s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}).Times(2)
+ gomock.InOrder(
+ // "test" is Me, i am known, and already on the channel
+ s.st.EXPECT().GetNick("test").Return(c.cfg.Me),
+ s.st.EXPECT().IsOn("#test1", "test").Return(&state.ChanPrivs{}, true),
+ // user1 is known, but not on the channel, so should be associated
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().IsOn("#test1", "user1").Return(nil, false),
+ s.st.EXPECT().Associate("#test1", "user1").Return(&state.ChanPrivs{}),
+ s.st.EXPECT().ChannelModes("#test1", "+o", "user1"),
+ )
+ for n, m := range map[string]string{
+ "user2": "",
+ "voice": "+v",
+ "halfop": "+h",
+ "op": "+o",
+ "admin": "+a",
+ "owner": "+q",
+ } {
+ calls := []*gomock.Call{
+ s.st.EXPECT().GetNick(n).Return(nil),
+ s.st.EXPECT().NewNick(n).Return(&state.Nick{Nick: n}),
+ s.st.EXPECT().IsOn("#test1", n).Return(nil, false),
+ s.st.EXPECT().Associate("#test1", n).Return(&state.ChanPrivs{}),
+ }
+ if m != "" {
+ calls = append(calls, s.st.EXPECT().ChannelModes("#test1", m, n))
+ }
+ gomock.InOrder(calls...)
+ }
+
+ // Send a couple of names replies (complete with trailing space)
+ c.h_353(ParseLine(":irc.server.org 353 test = #test1 :test @user1 user2 +voice "))
+ c.h_353(ParseLine(":irc.server.org 353 test = #test1 :%halfop @op &admin ~owner "))
+
+ // Check error paths -- send 353 for an unknown channel
+ s.st.EXPECT().GetChannel("#test2").Return(nil)
+ c.h_353(ParseLine(":irc.server.org 353 test = #test2 :test ~user3"))
+}
+
+// Test the handler for 671 (unreal specific)
+func Test671(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure 671 reply calls NickModes
+ gomock.InOrder(
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().NickModes("user1", "+z"),
+ )
+ c.h_671(ParseLine(":irc.server.org 671 test user1 :some ignored text"))
+
+ // Check error paths -- send a 671 for an unknown nick
+ s.st.EXPECT().GetNick("user2").Return(nil)
+ c.h_671(ParseLine(":irc.server.org 671 test user2 :some ignored text"))
+}
diff --git a/vendor/github.com/fluffle/goirc/client/line.go b/vendor/github.com/fluffle/goirc/client/line.go
new file mode 100644
index 0000000..bfa473a
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/line.go
@@ -0,0 +1,216 @@
+package client
+
+import (
+ "runtime"
+ "strings"
+ "time"
+
+ "github.com/fluffle/goirc/logging"
+)
+
+var tagsReplacer = strings.NewReplacer("\\:", ";", "\\s", " ", "\\r", "\r", "\\n", "\n")
+
+// We parse an incoming line into this struct. Line.Cmd is used as the trigger
+// name for incoming event handlers and is the IRC verb, the first sequence
+// of non-whitespace characters after ":nick!user@host", e.g. PRIVMSG.
+// Raw =~ ":nick!user@host cmd args[] :text"
+// Src == "nick!user@host"
+// Cmd == e.g. PRIVMSG, 332
+type Line struct {
+ Tags map[string]string
+ Nick, Ident, Host, Src string
+ Cmd, Raw string
+ Args []string
+ Time time.Time
+}
+
+// Copy returns a deep copy of the Line.
+func (l *Line) Copy() *Line {
+ nl := *l
+ nl.Args = make([]string, len(l.Args))
+ copy(nl.Args, l.Args)
+ if l.Tags != nil {
+ nl.Tags = make(map[string]string)
+ for k, v := range l.Tags {
+ nl.Tags[k] = v
+ }
+ }
+ return &nl
+}
+
+// Text returns the contents of the text portion of a line. This only really
+// makes sense for lines with a :text part, but there are a lot of them.
+func (line *Line) Text() string {
+ if len(line.Args) > 0 {
+ return line.Args[len(line.Args)-1]
+ }
+ return ""
+}
+
+// Target returns the contextual target of the line, usually the first Arg
+// for the IRC verb. If the line was broadcast from a channel, the target
+// will be that channel. If the line was sent directly by a user, the target
+// will be that user.
+func (line *Line) Target() string {
+ // TODO(fluffle): Add 005 CHANTYPES parsing for this?
+ switch line.Cmd {
+ case PRIVMSG, NOTICE, ACTION:
+ if !line.Public() {
+ return line.Nick
+ }
+ case CTCP, CTCPREPLY:
+ if !line.Public() {
+ return line.Nick
+ }
+ return line.Args[1]
+ }
+ if len(line.Args) > 0 {
+ return line.Args[0]
+ }
+ return ""
+}
+
+// Public returns true if the line is the result of an IRC user sending
+// a message to a channel the client has joined instead of directly
+// to the client.
+//
+// NOTE: This is very permissive, allowing all 4 RFC channel types even if
+// your server doesn't technically support them.
+func (line *Line) Public() bool {
+ switch line.Cmd {
+ case PRIVMSG, NOTICE, ACTION:
+ switch line.Args[0][0] {
+ case '#', '&', '+', '!':
+ return true
+ }
+ case CTCP, CTCPREPLY:
+ // CTCP prepends the CTCP verb to line.Args, thus for the message
+ // :nick!user@host PRIVMSG #foo :\001BAR baz\001
+ // line.Args contains: []string{"BAR", "#foo", "baz"}
+ // TODO(fluffle): Arguably this is broken, and we should have
+ // line.Args containing: []string{"#foo", "BAR", "baz"}
+ // ... OR change conn.Ctcp()'s argument order to be consistent.
+ switch line.Args[1][0] {
+ case '#', '&', '+', '!':
+ return true
+ }
+ }
+ return false
+}
+
+// ParseLine creates a Line from an incoming message from the IRC server.
+//
+// It contains special casing for CTCP messages, most notably CTCP ACTION.
+// All CTCP messages have the \001 bytes stripped from the message and the
+// CTCP command separated from any subsequent text. Then, CTCP ACTIONs are
+// rewritten such that Line.Cmd == ACTION. Other CTCP messages have Cmd
+// set to CTCP or CTCPREPLY, and the CTCP command prepended to line.Args.
+//
+// ParseLine also parses IRCv3 tags, if received. If a line does not have
+// the tags section, Line.Tags will be nil. Tags are optional, and will
+// only be included after the correct CAP command.
+//
+// http://ircv3.net/specs/core/capability-negotiation-3.1.html
+// http://ircv3.net/specs/core/message-tags-3.2.html
+func ParseLine(s string) *Line {
+ line := &Line{Raw: s}
+
+ if s == "" {
+ return nil
+ }
+
+ if s[0] == '@' {
+ var rawTags string
+ line.Tags = make(map[string]string)
+ if idx := strings.Index(s, " "); idx != -1 {
+ rawTags, s = s[1:idx], s[idx+1:]
+ } else {
+ return nil
+ }
+
+ // ; is represented as \: in a tag, so it's safe to split on ;
+ for _, tag := range strings.Split(rawTags, ";") {
+ if tag == "" {
+ continue
+ }
+
+ pair := strings.SplitN(tagsReplacer.Replace(tag), "=", 2)
+ if len(pair) < 2 {
+ line.Tags[tag] = ""
+ } else {
+ line.Tags[pair[0]] = pair[1]
+ }
+ }
+ }
+
+ if s[0] == ':' {
+ // remove a source and parse it
+ if idx := strings.Index(s, " "); idx != -1 {
+ line.Src, s = s[1:idx], s[idx+1:]
+ } else {
+ // pretty sure we shouldn't get here ...
+ return nil
+ }
+
+ // src can be the hostname of the irc server or a nick!user@host
+ line.Host = line.Src
+ nidx, uidx := strings.Index(line.Src, "!"), strings.Index(line.Src, "@")
+ if uidx != -1 && nidx != -1 {
+ line.Nick = line.Src[:nidx]
+ line.Ident = line.Src[nidx+1 : uidx]
+ line.Host = line.Src[uidx+1:]
+ }
+ }
+
+ // now we're here, we've parsed a :nick!user@host or :server off
+ // s should contain "cmd args[] :text"
+ args := strings.SplitN(s, " :", 2)
+ if len(args) > 1 {
+ args = append(strings.Fields(args[0]), args[1])
+ } else {
+ args = strings.Fields(args[0])
+ }
+ line.Cmd = strings.ToUpper(args[0])
+ if len(args) > 1 {
+ line.Args = args[1:]
+ }
+
+ // So, I think CTCP and (in particular) CTCP ACTION are better handled as
+ // separate events as opposed to forcing people to have gargantuan
+ // handlers to cope with the possibilities.
+ if (line.Cmd == PRIVMSG || line.Cmd == NOTICE) &&
+ len(line.Args[1]) > 2 &&
+ strings.HasPrefix(line.Args[1], "\001") &&
+ strings.HasSuffix(line.Args[1], "\001") {
+ // WOO, it's a CTCP message
+ t := strings.SplitN(strings.Trim(line.Args[1], "\001"), " ", 2)
+ if len(t) > 1 {
+ // Replace the line with the unwrapped CTCP
+ line.Args[1] = t[1]
+ }
+ if c := strings.ToUpper(t[0]); c == ACTION && line.Cmd == PRIVMSG {
+ // make a CTCP ACTION it's own event a-la PRIVMSG
+ line.Cmd = c
+ } else {
+ // otherwise, dispatch a generic CTCP/CTCPREPLY event that
+ // contains the type of CTCP in line.Args[0]
+ if line.Cmd == PRIVMSG {
+ line.Cmd = CTCP
+ } else {
+ line.Cmd = CTCPREPLY
+ }
+ line.Args = append([]string{c}, line.Args...)
+ }
+ }
+ return line
+}
+
+func (line *Line) argslen(minlen int) bool {
+ pc, _, _, _ := runtime.Caller(1)
+ fn := runtime.FuncForPC(pc)
+ if len(line.Args) <= minlen {
+ logging.Warn("%s: too few arguments: %s", fn.Name(), strings.Join(line.Args, " "))
+ return false
+ }
+ return true
+}
diff --git a/vendor/github.com/fluffle/goirc/client/line_test.go b/vendor/github.com/fluffle/goirc/client/line_test.go
new file mode 100644
index 0000000..88b758d
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/line_test.go
@@ -0,0 +1,186 @@
+package client
+
+import (
+ "reflect"
+ "testing"
+ "time"
+)
+
+func TestLineCopy(t *testing.T) {
+ l1 := &Line{
+ Tags: map[string]string{"foo": "bar", "fizz": "buzz"},
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host",
+ Src: "src",
+ Cmd: "cmd",
+ Raw: "raw",
+ Args: []string{"arg", "text"},
+ Time: time.Now(),
+ }
+
+ l2 := l1.Copy()
+
+ // Ugly. Couldn't be bothered to bust out reflect and actually think.
+ if l2.Tags == nil || l2.Tags["foo"] != "bar" || l2.Tags["fizz"] != "buzz" ||
+ l2.Nick != "nick" || l2.Ident != "ident" || l2.Host != "host" ||
+ l2.Src != "src" || l2.Cmd != "cmd" || l2.Raw != "raw" ||
+ l2.Args[0] != "arg" || l2.Args[1] != "text" || l2.Time != l1.Time {
+ t.Errorf("Line not copied correctly")
+ t.Errorf("l1: %#v\nl2: %#v", l1, l2)
+ }
+
+ // Now, modify l2 and verify l1 not changed
+ l2.Tags["foo"] = "baz"
+ l2.Nick = l2.Nick[1:]
+ l2.Ident = "foo"
+ l2.Host = ""
+ l2.Args[0] = l2.Args[0][1:]
+ l2.Args[1] = "bar"
+ l2.Time = time.Now()
+
+ if l2.Tags == nil || l2.Tags["foo"] != "baz" || l2.Tags["fizz"] != "buzz" ||
+ l1.Nick != "nick" || l1.Ident != "ident" || l1.Host != "host" ||
+ l1.Src != "src" || l1.Cmd != "cmd" || l1.Raw != "raw" ||
+ l1.Args[0] != "arg" || l1.Args[1] != "text" || l1.Time == l2.Time {
+ t.Errorf("Original modified when copy changed")
+ t.Errorf("l1: %#v\nl2: %#v", l1, l2)
+ }
+}
+
+func TestLineText(t *testing.T) {
+ tests := []struct {
+ in *Line
+ out string
+ }{
+ {&Line{}, ""},
+ {&Line{Args: []string{"one thing"}}, "one thing"},
+ {&Line{Args: []string{"one", "two"}}, "two"},
+ }
+
+ for i, test := range tests {
+ out := test.in.Text()
+ if out != test.out {
+ t.Errorf("test %d: expected: '%s', got '%s'", i, test.out, out)
+ }
+ }
+}
+
+func TestLineTarget(t *testing.T) {
+ tests := []struct {
+ in *Line
+ out string
+ }{
+ {&Line{}, ""},
+ {&Line{Cmd: JOIN, Args: []string{"#foo"}}, "#foo"},
+ {&Line{Cmd: PART, Args: []string{"#foo", "bye"}}, "#foo"},
+ {&Line{Cmd: PRIVMSG, Args: []string{"Me", "la"}, Nick: "Them"}, "Them"},
+ {&Line{Cmd: NOTICE, Args: []string{"Me", "la"}, Nick: "Them"}, "Them"},
+ {&Line{Cmd: ACTION, Args: []string{"Me", "la"}, Nick: "Them"}, "Them"},
+ {&Line{Cmd: CTCP, Args: []string{"PING", "Me", "1"}, Nick: "Them"}, "Them"},
+ {&Line{Cmd: CTCPREPLY, Args: []string{"PONG", "Me", "2"}, Nick: "Them"}, "Them"},
+ {&Line{Cmd: PRIVMSG, Args: []string{"#foo", "la"}, Nick: "Them"}, "#foo"},
+ {&Line{Cmd: NOTICE, Args: []string{"&foo", "la"}, Nick: "Them"}, "&foo"},
+ {&Line{Cmd: ACTION, Args: []string{"!foo", "la"}, Nick: "Them"}, "!foo"},
+ {&Line{Cmd: CTCP, Args: []string{"PING", "#foo", "1"}, Nick: "Them"}, "#foo"},
+ {&Line{Cmd: CTCPREPLY, Args: []string{"PONG", "#foo", "2"}, Nick: "Them"}, "#foo"},
+ }
+
+ for i, test := range tests {
+ out := test.in.Target()
+ if out != test.out {
+ t.Errorf("test %d: expected: '%s', got '%s'", i, test.out, out)
+ }
+ }
+}
+
+func TestLineTags(t *testing.T) {
+ tests := []struct {
+ in string
+ out *Line
+ }{
+ { // Make sure ERROR lines work
+ "ERROR :Closing Link: example.org (Too many user connections (global))",
+ &Line{
+ Nick: "",
+ Ident: "",
+ Host: "",
+ Src: "",
+ Cmd: ERROR,
+ Raw: "ERROR :Closing Link: example.org (Too many user connections (global))",
+ Args: []string{"Closing Link: example.org (Too many user connections (global))"},
+ },
+ },
+ { // Make sure non-tagged lines work
+ ":nick!ident@host.com PRIVMSG me :Hello",
+ &Line{
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host.com",
+ Src: "nick!ident@host.com",
+ Cmd: PRIVMSG,
+ Raw: ":nick!ident@host.com PRIVMSG me :Hello",
+ Args: []string{"me", "Hello"},
+ },
+ },
+ { // Tags example from the spec
+ "@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello",
+ &Line{
+ Tags: map[string]string{"aaa": "bbb", "ccc": "", "example.com/ddd": "eee"},
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host.com",
+ Src: "nick!ident@host.com",
+ Cmd: PRIVMSG,
+ Raw: "@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello",
+ Args: []string{"me", "Hello"},
+ },
+ },
+ { // Test escaped characters
+ "@\\:=\\:;\\s=\\s;\\r=\\r;\\n=\\n :nick!ident@host.com PRIVMSG me :Hello",
+ &Line{
+ Tags: map[string]string{";": ";", " ": " ", "\r": "\r", "\n": "\n"},
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host.com",
+ Src: "nick!ident@host.com",
+ Cmd: PRIVMSG,
+ Raw: "@\\:=\\:;\\s=\\s;\\r=\\r;\\n=\\n :nick!ident@host.com PRIVMSG me :Hello",
+ Args: []string{"me", "Hello"},
+ },
+ },
+ { // Skip empty tag
+ "@a=a; :nick!ident@host.com PRIVMSG me :Hello",
+ &Line{
+ Tags: map[string]string{"a": "a"},
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host.com",
+ Src: "nick!ident@host.com",
+ Cmd: PRIVMSG,
+ Raw: "@a=a; :nick!ident@host.com PRIVMSG me :Hello",
+ Args: []string{"me", "Hello"},
+ },
+ },
+ { // = in tag
+ "@a=a=a; :nick!ident@host.com PRIVMSG me :Hello",
+ &Line{
+ Tags: map[string]string{"a": "a=a"},
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host.com",
+ Src: "nick!ident@host.com",
+ Cmd: PRIVMSG,
+ Raw: "@a=a=a; :nick!ident@host.com PRIVMSG me :Hello",
+ Args: []string{"me", "Hello"},
+ },
+ },
+ }
+
+ for i, test := range tests {
+ got := ParseLine(test.in)
+ if !reflect.DeepEqual(got, test.out) {
+ t.Errorf("test %d:\nexpected %#v\ngot %#v", i, test.out, got)
+ }
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/mocknetconn_test.go b/vendor/github.com/fluffle/goirc/client/mocknetconn_test.go
new file mode 100644
index 0000000..e736c88
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/mocknetconn_test.go
@@ -0,0 +1,154 @@
+package client
+
+import (
+ "io"
+ "net"
+ "os"
+ "strings"
+ "testing"
+ "time"
+)
+
+type mockNetConn struct {
+ *testing.T
+
+ In, Out chan string
+ in, out chan []byte
+ die chan struct{}
+
+ closed bool
+ rt, wt time.Time
+}
+
+func MockNetConn(t *testing.T) *mockNetConn {
+ // Our mock connection is a testing object
+ m := &mockNetConn{T: t, die: make(chan struct{})}
+
+ // buffer input
+ m.In = make(chan string, 20)
+ m.in = make(chan []byte)
+ go func() {
+ for {
+ select {
+ case <-m.die:
+ return
+ case s := <-m.In:
+ m.in <- []byte(s)
+ }
+ }
+ }()
+
+ // buffer output
+ m.Out = make(chan string)
+ m.out = make(chan []byte, 20)
+ go func() {
+ for {
+ select {
+ case <-m.die:
+ return
+ case b := <-m.out:
+ m.Out <- string(b)
+ }
+ }
+ }()
+
+ return m
+}
+
+// Test helpers
+func (m *mockNetConn) Send(s string) {
+ m.In <- s + "\r\n"
+}
+
+func (m *mockNetConn) Expect(e string) {
+ select {
+ 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:
+ s = strings.Trim(s, "\r\n")
+ if e != s {
+ m.Errorf("Mock connection received unexpected value.\n\t"+
+ "Expected: '%s'\n\tGot: '%s'", e, s)
+ }
+ }
+}
+
+func (m *mockNetConn) ExpectNothing() {
+ select {
+ case <-time.After(time.Millisecond):
+ case s := <-m.Out:
+ s = strings.Trim(s, "\r\n")
+ m.Errorf("Mock connection received unexpected output.\n\t"+
+ "Expected nothing, got: '%s'", s)
+ }
+}
+
+// Implement net.Conn interface
+func (m *mockNetConn) Read(b []byte) (int, error) {
+ if m.Closed() {
+ return 0, os.ErrInvalid
+ }
+ l := 0
+ select {
+ case s := <-m.in:
+ l = len(s)
+ copy(b, s)
+ case <-m.die:
+ return 0, io.EOF
+ }
+ return l, nil
+}
+
+func (m *mockNetConn) Write(s []byte) (int, error) {
+ if m.Closed() {
+ return 0, os.ErrInvalid
+ }
+ b := make([]byte, len(s))
+ copy(b, s)
+ m.out <- b
+ return len(s), nil
+}
+
+func (m *mockNetConn) Close() error {
+ if m.Closed() {
+ return os.ErrInvalid
+ }
+ // Shut down *ALL* the goroutines!
+ // This will trigger an EOF event in Read() too
+ close(m.die)
+ return nil
+}
+
+func (m *mockNetConn) Closed() bool {
+ select {
+ case <-m.die:
+ return true
+ default:
+ return false
+ }
+}
+
+func (m *mockNetConn) LocalAddr() net.Addr {
+ return &net.IPAddr{IP: net.IPv4(127, 0, 0, 1)}
+}
+
+func (m *mockNetConn) RemoteAddr() net.Addr {
+ return &net.IPAddr{IP: net.IPv4(127, 0, 0, 1)}
+}
+
+func (m *mockNetConn) SetDeadline(t time.Time) error {
+ m.rt = t
+ m.wt = t
+ return nil
+}
+
+func (m *mockNetConn) SetReadDeadline(t time.Time) error {
+ m.rt = t
+ return nil
+}
+
+func (m *mockNetConn) SetWriteDeadline(t time.Time) error {
+ m.wt = t
+ return nil
+}
diff --git a/vendor/github.com/fluffle/goirc/client/state_handlers.go b/vendor/github.com/fluffle/goirc/client/state_handlers.go
new file mode 100644
index 0000000..847679c
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/state_handlers.go
@@ -0,0 +1,262 @@
+package client
+
+// this file contains the extra set of event handlers
+// to manage tracking state for an IRC connection
+
+import (
+ "strings"
+
+ "github.com/fluffle/goirc/logging"
+)
+
+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.stRemovers = append(conn.stRemovers, conn.handle(n, h))
+ }
+}
+
+func (conn *Conn) delSTHandlers() {
+ for _, h := range conn.stRemovers {
+ h.Remove()
+ }
+ conn.stRemovers = conn.stRemovers[:0]
+}
+
+// Handle NICK messages that need to update the state tracker
+func (conn *Conn) h_STNICK(line *Line) {
+ // all nicks should be handled the same way, our own included
+ conn.st.ReNick(line.Nick, line.Args[0])
+}
+
+// Handle JOINs to channels to maintain state
+func (conn *Conn) h_JOIN(line *Line) {
+ ch := conn.st.GetChannel(line.Args[0])
+ nk := conn.st.GetNick(line.Nick)
+ if ch == nil {
+ // first we've seen of this channel, so should be us joining it
+ // NOTE this will also take care of nk == nil && ch == nil
+ if !conn.Me().Equals(nk) {
+ logging.Warn("irc.JOIN(): JOIN to unknown channel %s received "+
+ "from (non-me) nick %s", line.Args[0], line.Nick)
+ return
+ }
+ conn.st.NewChannel(line.Args[0])
+ // since we don't know much about this channel, ask server for info
+ // we get the channel users automatically in 353 and the channel
+ // topic in 332 on join, so we just need to get the modes
+ conn.Mode(line.Args[0])
+ // sending a WHO for the channel is MUCH more efficient than
+ // triggering a WHOIS on every nick from the 353 handler
+ conn.Who(line.Args[0])
+ }
+ if nk == nil {
+ // this is the first we've seen of this nick
+ conn.st.NewNick(line.Nick)
+ conn.st.NickInfo(line.Nick, line.Ident, line.Host, "")
+ // since we don't know much about this nick, ask server for info
+ conn.Who(line.Nick)
+ }
+ // this takes care of both nick and channel linking \o/
+ conn.st.Associate(line.Args[0], line.Nick)
+}
+
+// Handle PARTs from channels to maintain state
+func (conn *Conn) h_PART(line *Line) {
+ conn.st.Dissociate(line.Args[0], line.Nick)
+}
+
+// Handle KICKs from channels to maintain state
+func (conn *Conn) h_KICK(line *Line) {
+ if !line.argslen(1) {
+ return
+ }
+ // XXX: this won't handle autorejoining channels on KICK
+ // it's trivial to do this in a seperate handler...
+ conn.st.Dissociate(line.Args[0], line.Args[1])
+}
+
+// Handle other people's QUITs
+func (conn *Conn) h_QUIT(line *Line) {
+ conn.st.DelNick(line.Nick)
+}
+
+// Handle MODE changes for channels we know about (and our nick personally)
+func (conn *Conn) h_MODE(line *Line) {
+ if !line.argslen(1) {
+ return
+ }
+ if ch := conn.st.GetChannel(line.Args[0]); ch != nil {
+ // channel modes first
+ conn.st.ChannelModes(line.Args[0], line.Args[1], line.Args[2:]...)
+ } else if nk := conn.st.GetNick(line.Args[0]); nk != nil {
+ // nick mode change, should be us
+ if !conn.Me().Equals(nk) {
+ logging.Warn("irc.MODE(): recieved MODE %s for (non-me) nick %s",
+ line.Args[1], line.Args[0])
+ return
+ }
+ conn.st.NickModes(line.Args[0], line.Args[1])
+ } else {
+ logging.Warn("irc.MODE(): not sure what to do with MODE %s",
+ strings.Join(line.Args, " "))
+ }
+}
+
+// Handle TOPIC changes for channels
+func (conn *Conn) h_TOPIC(line *Line) {
+ if !line.argslen(1) {
+ return
+ }
+ if ch := conn.st.GetChannel(line.Args[0]); ch != nil {
+ conn.st.Topic(line.Args[0], line.Args[1])
+ } else {
+ logging.Warn("irc.TOPIC(): topic change on unknown channel %s",
+ line.Args[0])
+ }
+}
+
+// Handle 311 whois reply
+func (conn *Conn) h_311(line *Line) {
+ if !line.argslen(5) {
+ return
+ }
+ if nk := conn.st.GetNick(line.Args[1]); (nk != nil) && !conn.Me().Equals(nk) {
+ conn.st.NickInfo(line.Args[1], line.Args[2], line.Args[3], line.Args[5])
+ } else {
+ logging.Warn("irc.311(): received WHOIS info for unknown nick %s",
+ line.Args[1])
+ }
+}
+
+// Handle 324 mode reply
+func (conn *Conn) h_324(line *Line) {
+ if !line.argslen(2) {
+ return
+ }
+ if ch := conn.st.GetChannel(line.Args[1]); ch != nil {
+ conn.st.ChannelModes(line.Args[1], line.Args[2], line.Args[3:]...)
+ } else {
+ logging.Warn("irc.324(): received MODE settings for unknown channel %s",
+ line.Args[1])
+ }
+}
+
+// Handle 332 topic reply on join to channel
+func (conn *Conn) h_332(line *Line) {
+ if !line.argslen(2) {
+ return
+ }
+ if ch := conn.st.GetChannel(line.Args[1]); ch != nil {
+ conn.st.Topic(line.Args[1], line.Args[2])
+ } else {
+ logging.Warn("irc.332(): received TOPIC value for unknown channel %s",
+ line.Args[1])
+ }
+}
+
+// Handle 352 who reply
+func (conn *Conn) h_352(line *Line) {
+ if !line.argslen(5) {
+ return
+ }
+ nk := conn.st.GetNick(line.Args[5])
+ if nk == nil {
+ logging.Warn("irc.352(): received WHO reply for unknown nick %s",
+ line.Args[5])
+ return
+ }
+ if conn.Me().Equals(nk) {
+ return
+ }
+ // XXX: do we care about the actual server the nick is on?
+ // or the hop count to this server?
+ // last arg contains "<hop count> <real name>"
+ a := strings.SplitN(line.Args[len(line.Args)-1], " ", 2)
+ conn.st.NickInfo(nk.Nick, line.Args[2], line.Args[3], a[1])
+ if !line.argslen(6) {
+ return
+ }
+ if idx := strings.Index(line.Args[6], "*"); idx != -1 {
+ conn.st.NickModes(nk.Nick, "+o")
+ }
+ if idx := strings.Index(line.Args[6], "B"); idx != -1 {
+ conn.st.NickModes(nk.Nick, "+B")
+ }
+ if idx := strings.Index(line.Args[6], "H"); idx != -1 {
+ conn.st.NickModes(nk.Nick, "+i")
+ }
+}
+
+// Handle 353 names reply
+func (conn *Conn) h_353(line *Line) {
+ if !line.argslen(2) {
+ return
+ }
+ if ch := conn.st.GetChannel(line.Args[2]); ch != nil {
+ nicks := strings.Split(line.Args[len(line.Args)-1], " ")
+ for _, nick := range nicks {
+ // UnrealIRCd's coders are lazy and leave a trailing space
+ if nick == "" {
+ continue
+ }
+ switch c := nick[0]; c {
+ case '~', '&', '@', '%', '+':
+ nick = nick[1:]
+ fallthrough
+ default:
+ if conn.st.GetNick(nick) == nil {
+ // we don't know this nick yet!
+ conn.st.NewNick(nick)
+ }
+ if _, ok := conn.st.IsOn(ch.Name, nick); !ok {
+ // This nick isn't associated with this channel yet!
+ conn.st.Associate(ch.Name, nick)
+ }
+ switch c {
+ case '~':
+ conn.st.ChannelModes(ch.Name, "+q", nick)
+ case '&':
+ conn.st.ChannelModes(ch.Name, "+a", nick)
+ case '@':
+ conn.st.ChannelModes(ch.Name, "+o", nick)
+ case '%':
+ conn.st.ChannelModes(ch.Name, "+h", nick)
+ case '+':
+ conn.st.ChannelModes(ch.Name, "+v", nick)
+ }
+ }
+ }
+ } else {
+ logging.Warn("irc.353(): received NAMES list for unknown channel %s",
+ line.Args[2])
+ }
+}
+
+// Handle 671 whois reply (nick connected via SSL)
+func (conn *Conn) h_671(line *Line) {
+ if !line.argslen(1) {
+ return
+ }
+ if nk := conn.st.GetNick(line.Args[1]); nk != nil {
+ conn.st.NickModes(nk.Nick, "+z")
+ } else {
+ logging.Warn("irc.671(): received WHOIS SSL info for unknown nick %s",
+ line.Args[1])
+ }
+}