From 354da79bb2edaa1af7d909d2774e7d67eb4e198c Mon Sep 17 00:00:00 2001 From: Dimitri Sokolyuk Date: Tue, 23 Jan 2018 18:17:51 +0100 Subject: Add vendor --- vendor/github.com/fluffle/goirc/client/commands.go | 304 +++++++++++ .../fluffle/goirc/client/commands_test.go | 205 ++++++++ .../github.com/fluffle/goirc/client/connection.go | 581 ++++++++++++++++++++ .../fluffle/goirc/client/connection_test.go | 585 +++++++++++++++++++++ vendor/github.com/fluffle/goirc/client/dispatch.go | 202 +++++++ .../fluffle/goirc/client/dispatch_test.go | 201 +++++++ vendor/github.com/fluffle/goirc/client/doc.go | 34 ++ vendor/github.com/fluffle/goirc/client/handlers.go | 105 ++++ .../fluffle/goirc/client/handlers_test.go | 451 ++++++++++++++++ vendor/github.com/fluffle/goirc/client/line.go | 216 ++++++++ .../github.com/fluffle/goirc/client/line_test.go | 186 +++++++ .../fluffle/goirc/client/mocknetconn_test.go | 154 ++++++ .../fluffle/goirc/client/state_handlers.go | 262 +++++++++ 13 files changed, 3486 insertions(+) create mode 100644 vendor/github.com/fluffle/goirc/client/commands.go create mode 100644 vendor/github.com/fluffle/goirc/client/commands_test.go create mode 100644 vendor/github.com/fluffle/goirc/client/connection.go create mode 100644 vendor/github.com/fluffle/goirc/client/connection_test.go create mode 100644 vendor/github.com/fluffle/goirc/client/dispatch.go create mode 100644 vendor/github.com/fluffle/goirc/client/dispatch_test.go create mode 100644 vendor/github.com/fluffle/goirc/client/doc.go create mode 100644 vendor/github.com/fluffle/goirc/client/handlers.go create mode 100644 vendor/github.com/fluffle/goirc/client/handlers_test.go create mode 100644 vendor/github.com/fluffle/goirc/client/line.go create mode 100644 vendor/github.com/fluffle/goirc/client/line_test.go create mode 100644 vendor/github.com/fluffle/goirc/client/mocknetconn_test.go create mode 100644 vendor/github.com/fluffle/goirc/client/state_handlers.go (limited to 'vendor/github.com/fluffle/goirc/client') 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 " " + 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]) + } +} -- cgit v1.2.3