summaryrefslogtreecommitdiff
path: root/vendor/github.com/fluffle/goirc
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/fluffle/goirc')
-rw-r--r--vendor/github.com/fluffle/goirc/.gitignore8
-rw-r--r--vendor/github.com/fluffle/goirc/.travis.yml19
-rw-r--r--vendor/github.com/fluffle/goirc/LICENSE27
-rw-r--r--vendor/github.com/fluffle/goirc/README.md119
-rw-r--r--vendor/github.com/fluffle/goirc/client.go104
-rw-r--r--vendor/github.com/fluffle/goirc/client/commands.go304
-rw-r--r--vendor/github.com/fluffle/goirc/client/commands_test.go205
-rw-r--r--vendor/github.com/fluffle/goirc/client/connection.go581
-rw-r--r--vendor/github.com/fluffle/goirc/client/connection_test.go585
-rw-r--r--vendor/github.com/fluffle/goirc/client/dispatch.go202
-rw-r--r--vendor/github.com/fluffle/goirc/client/dispatch_test.go201
-rw-r--r--vendor/github.com/fluffle/goirc/client/doc.go34
-rw-r--r--vendor/github.com/fluffle/goirc/client/handlers.go105
-rw-r--r--vendor/github.com/fluffle/goirc/client/handlers_test.go451
-rw-r--r--vendor/github.com/fluffle/goirc/client/line.go216
-rw-r--r--vendor/github.com/fluffle/goirc/client/line_test.go186
-rw-r--r--vendor/github.com/fluffle/goirc/client/mocknetconn_test.go154
-rw-r--r--vendor/github.com/fluffle/goirc/client/state_handlers.go262
-rw-r--r--vendor/github.com/fluffle/goirc/logging/logging.go43
-rw-r--r--vendor/github.com/fluffle/goirc/state/channel.go350
-rw-r--r--vendor/github.com/fluffle/goirc/state/channel_test.go176
-rw-r--r--vendor/github.com/fluffle/goirc/state/mock_tracker.go201
-rw-r--r--vendor/github.com/fluffle/goirc/state/nick.go200
-rw-r--r--vendor/github.com/fluffle/goirc/state/nick_test.go88
-rw-r--r--vendor/github.com/fluffle/goirc/state/tracker.go369
-rw-r--r--vendor/github.com/fluffle/goirc/state/tracker_test.go564
-rw-r--r--vendor/github.com/fluffle/goirc/vims1
27 files changed, 5755 insertions, 0 deletions
diff --git a/vendor/github.com/fluffle/goirc/.gitignore b/vendor/github.com/fluffle/goirc/.gitignore
new file mode 100644
index 0000000..1cbcab5
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/.gitignore
@@ -0,0 +1,8 @@
+/gobot
+*.[568]
+_obj/
+_test/
+*.swp
+*~
+*.out
+/.gitconfig
diff --git a/vendor/github.com/fluffle/goirc/.travis.yml b/vendor/github.com/fluffle/goirc/.travis.yml
new file mode 100644
index 0000000..420a9a8
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/.travis.yml
@@ -0,0 +1,19 @@
+---
+language: go
+
+go:
+ - 1.5
+ - 1.5.3
+ - 1.6
+
+sudo : false
+
+notifications:
+ irc:
+ channels:
+ - "irc.pl0rt.org#sp0rklf"
+ skip_join: true
+
+script:
+ - if [ "$TRAVIS_REPO_SLUG" != "fluffle/goirc" ] ; then ln -s "$HOME/gopath/src/github.com/$TRAVIS_REPO_SLUG" /home/travis/gopath/src/github.com/fluffle/goirc ; fi
+ - go test -v ./...
diff --git a/vendor/github.com/fluffle/goirc/LICENSE b/vendor/github.com/fluffle/goirc/LICENSE
new file mode 100644
index 0000000..e1b4d5c
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2009+ Alex Bramley. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/fluffle/goirc/README.md b/vendor/github.com/fluffle/goirc/README.md
new file mode 100644
index 0000000..2c12e84
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/README.md
@@ -0,0 +1,119 @@
+[![Build Status](https://api.travis-ci.org/fluffle/goirc.svg)](https://travis-ci.org/fluffle/goirc)
+
+GoIRC Client Framework
+======================
+
+### Acquiring and Building
+
+Pretty simple, really:
+
+ go get github.com/fluffle/goirc/client
+
+There is some example code that demonstrates usage of the library in `client.go`. This will connect to freenode and join `#go-nuts` by default, so be careful ;-)
+
+See `fix/goirc.go` and the README there for a quick way to migrate from the
+old `go1` API.
+
+### Using the framework
+
+Synopsis:
+```go
+package main
+
+import (
+ "crypto/tls"
+ "fmt"
+
+ irc "github.com/fluffle/goirc/client"
+)
+
+func main() {
+ // Creating a simple IRC client is simple.
+ c := irc.SimpleClient("nick")
+
+ // Or, create a config and fiddle with it first:
+ cfg := irc.NewConfig("nick")
+ cfg.SSL = true
+ cfg.SSLConfig = &tls.Config{ServerName: "irc.freenode.net"}
+ cfg.Server = "irc.freenode.net:7000"
+ cfg.NewNick = func(n string) string { return n + "^" }
+ c = irc.Client(cfg)
+
+ // Add handlers to do things here!
+ // e.g. join a channel on connect.
+ c.HandleFunc(irc.CONNECTED,
+ func(conn *irc.Conn, line *irc.Line) { conn.Join("#channel") })
+ // And a signal on disconnect
+ quit := make(chan bool)
+ c.HandleFunc(irc.DISCONNECTED,
+ func(conn *irc.Conn, line *irc.Line) { quit <- true })
+
+ // Tell client to connect.
+ if err := c.Connect(); err != nil {
+ fmt.Printf("Connection error: %s\n", err.Error())
+ }
+
+ // With a "simple" client, set Server before calling Connect...
+ c.Config().Server = "irc.freenode.net"
+
+ // ... or, use ConnectTo instead.
+ if err := c.ConnectTo("irc.freenode.net"); err != nil {
+ fmt.Printf("Connection error: %s\n", err.Error())
+ }
+
+ // Wait for disconnect
+ <-quit
+}
+```
+
+The test client provides a good (if basic) example of how to use the framework.
+Reading `client/handlers.go` gives a more in-depth look at how handlers can be
+written. Commands to be sent to the server (e.g. PRIVMSG) are methods of the
+main `*Conn` struct, and can be found in `client/commands.go` (not all of the
+possible IRC commands are implemented yet). Events are produced directly from
+the messages from the IRC server, so you have to handle e.g. "332" for
+`RPL_TOPIC` to get the topic for a channel.
+
+The vast majority of handlers implemented within the framework deal with state
+tracking of all nicks in any channels that the client is also present in. These
+handlers are in `client/state_handlers.go`. State tracking is optional, disabled
+by default, and can be enabled and disabled by calling `EnableStateTracking()`
+and `DisableStateTracking()` respectively. Doing this while connected to an IRC
+server will probably result in an inconsistent state and a lot of warnings to
+STDERR ;-)
+
+### Misc.
+
+Sorry the documentation is crap. Use the source, Luke.
+
+[Feedback](mailto:a.bramley@gmail.com) on design decisions is welcome. I am
+indebted to Matt Gruen for his work on
+[go-bot](http://code.google.com/p/go-bot/source/browse/irc.go) which inspired
+the re-organisation and channel-based communication structure of `*Conn.send()`
+and `*Conn.recv()`. I'm sure things could be more asynchronous, still.
+
+This code is (c) 2009-15 Alex Bramley, and released under the same licence terms
+as Go itself.
+
+Contributions gratefully received from:
+
+ - [3onyc](https://github.com/3onyc)
+ - [bramp](https://github.com/bramp)
+ - [cgt](https://github.com/cgt)
+ - [iopred](https://github.com/iopred)
+ - [Krayons](https://github.com/Krayons)
+ - [StalkR](https://github.com/StalkR)
+ - [sztanpet](https://github.com/sztanpet)
+ - [wathiede](https://github.com/wathiede)
+ - [scrapbird](https://github.com/scrapbird)
+ - [soul9](https://github.com/soul9)
+ - [jakebailey](https://github.com/jakebailey)
+ - [stapelberg](https://github.com/stapelberg)
+
+And thanks to the following for minor doc/fix PRs:
+
+ - [tmcarr](https://github.com/tmcarr)
+ - [Gentux](https://github.com/Gentux)
+ - [kidanger](https://github.com/kidanger)
+ - [ripcurld00d](https://github.com/ripcurld00d)
+ - [bob-smith](https://github.com/bob-smith)
diff --git a/vendor/github.com/fluffle/goirc/client.go b/vendor/github.com/fluffle/goirc/client.go
new file mode 100644
index 0000000..b71d706
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client.go
@@ -0,0 +1,104 @@
+package main
+
+import (
+ "bufio"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+
+ irc "github.com/fluffle/goirc/client"
+ "github.com/fluffle/goirc/logging/glog"
+)
+
+var host *string = flag.String("host", "irc.freenode.net", "IRC server")
+var channel *string = flag.String("channel", "#go-nuts", "IRC channel")
+
+func main() {
+ flag.Parse()
+ glog.Init()
+
+ // create new IRC connection
+ c := irc.SimpleClient("GoTest", "gotest")
+ c.EnableStateTracking()
+ c.HandleFunc("connected",
+ func(conn *irc.Conn, line *irc.Line) { conn.Join(*channel) })
+
+ // Set up a handler to notify of disconnect events.
+ quit := make(chan bool)
+ c.HandleFunc("disconnected",
+ func(conn *irc.Conn, line *irc.Line) { quit <- true })
+
+ // set up a goroutine to read commands from stdin
+ in := make(chan string, 4)
+ reallyquit := false
+ go func() {
+ con := bufio.NewReader(os.Stdin)
+ for {
+ s, err := con.ReadString('\n')
+ if err != nil {
+ // wha?, maybe ctrl-D...
+ close(in)
+ break
+ }
+ // no point in sending empty lines down the channel
+ if len(s) > 2 {
+ in <- s[0 : len(s)-1]
+ }
+ }
+ }()
+
+ // set up a goroutine to do parsey things with the stuff from stdin
+ go func() {
+ for cmd := range in {
+ if cmd[0] == ':' {
+ switch idx := strings.Index(cmd, " "); {
+ case cmd[1] == 'd':
+ fmt.Printf(c.String())
+ case cmd[1] == 'n':
+ parts := strings.Split(cmd, " ")
+ username := strings.TrimSpace(parts[1])
+ channelname := strings.TrimSpace(parts[2])
+ _, userIsOn := c.StateTracker().IsOn(channelname, username)
+ fmt.Printf("Checking if %s is in %s Online: %t\n", username, channelname, userIsOn)
+ case cmd[1] == 'f':
+ if len(cmd) > 2 && cmd[2] == 'e' {
+ // enable flooding
+ c.Config().Flood = true
+ } else if len(cmd) > 2 && cmd[2] == 'd' {
+ // disable flooding
+ c.Config().Flood = false
+ }
+ for i := 0; i < 20; i++ {
+ c.Privmsg("#", "flood test!")
+ }
+ case idx == -1:
+ continue
+ case cmd[1] == 'q':
+ reallyquit = true
+ c.Quit(cmd[idx+1 : len(cmd)])
+ case cmd[1] == 's':
+ reallyquit = true
+ c.Close()
+ case cmd[1] == 'j':
+ c.Join(cmd[idx+1 : len(cmd)])
+ case cmd[1] == 'p':
+ c.Part(cmd[idx+1 : len(cmd)])
+ }
+ } else {
+ c.Raw(cmd)
+ }
+ }
+ }()
+
+ for !reallyquit {
+ // connect to server
+ if err := c.ConnectTo(*host); err != nil {
+ fmt.Printf("Connection error: %s\n", err)
+ return
+ }
+
+ // wait on quit channel
+ <-quit
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/commands.go b/vendor/github.com/fluffle/goirc/client/commands.go
new file mode 100644
index 0000000..101c7d3
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/commands.go
@@ -0,0 +1,304 @@
+package client
+
+import (
+ "fmt"
+ "strings"
+)
+
+const (
+ REGISTER = "REGISTER"
+ CONNECTED = "CONNECTED"
+ DISCONNECTED = "DISCONNECTED"
+ ACTION = "ACTION"
+ AWAY = "AWAY"
+ CAP = "CAP"
+ CTCP = "CTCP"
+ CTCPREPLY = "CTCPREPLY"
+ ERROR = "ERROR"
+ INVITE = "INVITE"
+ JOIN = "JOIN"
+ KICK = "KICK"
+ MODE = "MODE"
+ NICK = "NICK"
+ NOTICE = "NOTICE"
+ OPER = "OPER"
+ PART = "PART"
+ PASS = "PASS"
+ PING = "PING"
+ PONG = "PONG"
+ PRIVMSG = "PRIVMSG"
+ QUIT = "QUIT"
+ TOPIC = "TOPIC"
+ USER = "USER"
+ VERSION = "VERSION"
+ VHOST = "VHOST"
+ WHO = "WHO"
+ WHOIS = "WHOIS"
+ defaultSplit = 450
+)
+
+// cutNewLines() pares down a string to the part before the first "\r" or "\n".
+func cutNewLines(s string) string {
+ r := strings.SplitN(s, "\r", 2)
+ r = strings.SplitN(r[0], "\n", 2)
+ return r[0]
+}
+
+// indexFragment looks for the last sentence split-point (defined as one of
+// the punctuation characters .:;,!?"' followed by a space) in the string s
+// and returns the index in the string after that split-point. If no split-
+// point is found it returns the index after the last space in s, or -1.
+func indexFragment(s string) int {
+ max := -1
+ for _, sep := range []string{". ", ": ", "; ", ", ", "! ", "? ", "\" ", "' "} {
+ if idx := strings.LastIndex(s, sep); idx > max {
+ max = idx
+ }
+ }
+ if max > 0 {
+ return max + 2
+ }
+ if idx := strings.LastIndex(s, " "); idx > 0 {
+ return idx + 1
+ }
+ return -1
+}
+
+// splitMessage splits a message > splitLen chars at:
+// 1. the end of the last sentence fragment before splitLen
+// 2. the end of the last word before splitLen
+// 3. splitLen itself
+func splitMessage(msg string, splitLen int) (msgs []string) {
+ // This is quite short ;-)
+ if splitLen < 13 {
+ splitLen = defaultSplit
+ }
+ for len(msg) > splitLen {
+ idx := indexFragment(msg[:splitLen-3])
+ if idx < 0 {
+ idx = splitLen - 3
+ }
+ msgs = append(msgs, msg[:idx]+"...")
+ msg = msg[idx:]
+ }
+ return append(msgs, msg)
+}
+
+// Raw sends a raw line to the server, should really only be used for
+// debugging purposes but may well come in handy.
+func (conn *Conn) Raw(rawline string) {
+ // Avoid command injection by enforcing one command per line.
+ conn.out <- cutNewLines(rawline)
+}
+
+// Pass sends a PASS command to the server.
+// PASS password
+func (conn *Conn) Pass(password string) { conn.Raw(PASS + " " + password) }
+
+// Nick sends a NICK command to the server.
+// NICK nick
+func (conn *Conn) Nick(nick string) { conn.Raw(NICK + " " + nick) }
+
+// User sends a USER command to the server.
+// USER ident 12 * :name
+func (conn *Conn) User(ident, name string) {
+ conn.Raw(USER + " " + ident + " 12 * :" + name)
+}
+
+// Join sends a JOIN command to the server with an optional key.
+// JOIN channel [key]
+func (conn *Conn) Join(channel string, key ...string) {
+ k := ""
+ if len(key) > 0 {
+ k = " " + key[0]
+ }
+ conn.Raw(JOIN + " " + channel + k)
+}
+
+// Part sends a PART command to the server with an optional part message.
+// PART channel [:message]
+func (conn *Conn) Part(channel string, message ...string) {
+ msg := strings.Join(message, " ")
+ if msg != "" {
+ msg = " :" + msg
+ }
+ conn.Raw(PART + " " + channel + msg)
+}
+
+// Kick sends a KICK command to remove a nick from a channel.
+// KICK channel nick [:message]
+func (conn *Conn) Kick(channel, nick string, message ...string) {
+ msg := strings.Join(message, " ")
+ if msg != "" {
+ msg = " :" + msg
+ }
+ conn.Raw(KICK + " " + channel + " " + nick + msg)
+}
+
+// Quit sends a QUIT command to the server with an optional quit message.
+// QUIT [:message]
+func (conn *Conn) Quit(message ...string) {
+ msg := strings.Join(message, " ")
+ if msg == "" {
+ msg = conn.cfg.QuitMessage
+ }
+ conn.Raw(QUIT + " :" + msg)
+}
+
+// Whois sends a WHOIS command to the server.
+// WHOIS nick
+func (conn *Conn) Whois(nick string) { conn.Raw(WHOIS + " " + nick) }
+
+// Who sends a WHO command to the server.
+// WHO nick
+func (conn *Conn) Who(nick string) { conn.Raw(WHO + " " + nick) }
+
+// Privmsg sends a PRIVMSG to the target nick or channel t.
+// If msg is longer than Config.SplitLen characters, multiple PRIVMSGs
+// will be sent to the target containing sequential parts of msg.
+// PRIVMSG t :msg
+func (conn *Conn) Privmsg(t, msg string) {
+ prefix := PRIVMSG + " " + t + " :"
+ for _, s := range splitMessage(msg, conn.cfg.SplitLen) {
+ conn.Raw(prefix + s)
+ }
+}
+
+// Privmsgln is the variadic version of Privmsg that formats the message
+// that is sent to the target nick or channel t using the
+// fmt.Sprintln function.
+// Note: Privmsgln doesn't add the '\n' character at the end of the message.
+func (conn *Conn) Privmsgln(t string, a ...interface{}) {
+ msg := fmt.Sprintln(a...)
+ // trimming the new-line character added by the fmt.Sprintln function,
+ // since it's irrelevant.
+ msg = msg[:len(msg)-1]
+ conn.Privmsg(t, msg)
+}
+
+// Privmsgf is the variadic version of Privmsg that formats the message
+// that is sent to the target nick or channel t using the
+// fmt.Sprintf function.
+func (conn *Conn) Privmsgf(t, format string, a ...interface{}) {
+ msg := fmt.Sprintf(format, a...)
+ conn.Privmsg(t, msg)
+}
+
+// Notice sends a NOTICE to the target nick or channel t.
+// If msg is longer than Config.SplitLen characters, multiple NOTICEs
+// will be sent to the target containing sequential parts of msg.
+// NOTICE t :msg
+func (conn *Conn) Notice(t, msg string) {
+ for _, s := range splitMessage(msg, conn.cfg.SplitLen) {
+ conn.Raw(NOTICE + " " + t + " :" + s)
+ }
+}
+
+// Ctcp sends a (generic) CTCP message to the target nick
+// or channel t, with an optional argument.
+// PRIVMSG t :\001CTCP arg\001
+func (conn *Conn) Ctcp(t, ctcp string, arg ...string) {
+ // We need to split again here to ensure
+ for _, s := range splitMessage(strings.Join(arg, " "), conn.cfg.SplitLen) {
+ if s != "" {
+ s = " " + s
+ }
+ // Using Raw rather than PRIVMSG here to avoid double-split problems.
+ conn.Raw(PRIVMSG + " " + t + " :\001" + strings.ToUpper(ctcp) + s + "\001")
+ }
+}
+
+// CtcpReply sends a (generic) CTCP reply to the target nick
+// or channel t, with an optional argument.
+// NOTICE t :\001CTCP arg\001
+func (conn *Conn) CtcpReply(t, ctcp string, arg ...string) {
+ for _, s := range splitMessage(strings.Join(arg, " "), conn.cfg.SplitLen) {
+ if s != "" {
+ s = " " + s
+ }
+ // Using Raw rather than NOTICE here to avoid double-split problems.
+ conn.Raw(NOTICE + " " + t + " :\001" + strings.ToUpper(ctcp) + s + "\001")
+ }
+}
+
+// Version sends a CTCP "VERSION" to the target nick or channel t.
+func (conn *Conn) Version(t string) { conn.Ctcp(t, VERSION) }
+
+// Action sends a CTCP "ACTION" to the target nick or channel t.
+func (conn *Conn) Action(t, msg string) { conn.Ctcp(t, ACTION, msg) }
+
+// Topic() sends a TOPIC command for a channel.
+// If no topic is provided this requests that a 332 response is sent by the
+// server for that channel, which can then be handled to retrieve the current
+// channel topic. If a topic is provided the channel's topic will be set.
+// TOPIC channel
+// TOPIC channel :topic
+func (conn *Conn) Topic(channel string, topic ...string) {
+ t := strings.Join(topic, " ")
+ if t != "" {
+ t = " :" + t
+ }
+ conn.Raw(TOPIC + " " + channel + t)
+}
+
+// Mode sends a MODE command for a target nick or channel t.
+// If no mode strings are provided this requests that a 324 response is sent
+// by the server for the target. Otherwise the mode strings are concatenated
+// with spaces and sent to the server. This allows e.g.
+// conn.Mode("#channel", "+nsk", "mykey")
+//
+// MODE t
+// MODE t modestring
+func (conn *Conn) Mode(t string, modestring ...string) {
+ mode := strings.Join(modestring, " ")
+ if mode != "" {
+ mode = " " + mode
+ }
+ conn.Raw(MODE + " " + t + mode)
+}
+
+// Away sends an AWAY command to the server.
+// If a message is provided it sets the client's away status with that message,
+// otherwise it resets the client's away status.
+// AWAY
+// AWAY :message
+func (conn *Conn) Away(message ...string) {
+ msg := strings.Join(message, " ")
+ if msg != "" {
+ msg = " :" + msg
+ }
+ conn.Raw(AWAY + msg)
+}
+
+// Invite sends an INVITE command to the server.
+// INVITE nick channel
+func (conn *Conn) Invite(nick, channel string) {
+ conn.Raw(INVITE + " " + nick + " " + channel)
+}
+
+// Oper sends an OPER command to the server.
+// OPER user pass
+func (conn *Conn) Oper(user, pass string) { conn.Raw(OPER + " " + user + " " + pass) }
+
+// VHost sends a VHOST command to the server.
+// VHOST user pass
+func (conn *Conn) VHost(user, pass string) { conn.Raw(VHOST + " " + user + " " + pass) }
+
+// Ping sends a PING command to the server, which should PONG.
+// PING :message
+func (conn *Conn) Ping(message string) { conn.Raw(PING + " :" + message) }
+
+// Pong sends a PONG command to the server.
+// PONG :message
+func (conn *Conn) Pong(message string) { conn.Raw(PONG + " :" + message) }
+
+// Cap sends a CAP command to the server.
+// CAP subcommand
+// CAP subcommand :message
+func (conn *Conn) Cap(subcommmand string, capabilities ...string) {
+ if len(capabilities) == 0 {
+ conn.Raw(CAP + " " + subcommmand)
+ } else {
+ conn.Raw(CAP + " " + subcommmand + " :" + strings.Join(capabilities, " "))
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/commands_test.go b/vendor/github.com/fluffle/goirc/client/commands_test.go
new file mode 100644
index 0000000..15a8a05
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/commands_test.go
@@ -0,0 +1,205 @@
+package client
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestCutNewLines(t *testing.T) {
+ tests := []struct{ in, out string }{
+ {"", ""},
+ {"foo bar", "foo bar"},
+ {"foo bar\rbaz", "foo bar"},
+ {"foo bar\nbaz", "foo bar"},
+ {"blorp\r\n\r\nbloop", "blorp"},
+ {"\n\rblaap", ""},
+ {"\r\n", ""},
+ {"boo\\r\\n\\n\r", "boo\\r\\n\\n"},
+ }
+ for i, test := range tests {
+ out := cutNewLines(test.in)
+ if test.out != out {
+ t.Errorf("test %d: expected %q, got %q", i, test.out, out)
+ }
+ }
+}
+
+func TestIndexFragment(t *testing.T) {
+ tests := []struct {
+ in string
+ out int
+ }{
+ {"", -1},
+ {"foobarbaz", -1},
+ {"foo bar baz", 8},
+ {"foo. bar baz", 5},
+ {"foo: bar baz", 5},
+ {"foo; bar baz", 5},
+ {"foo, bar baz", 5},
+ {"foo! bar baz", 5},
+ {"foo? bar baz", 5},
+ {"foo\" bar baz", 5},
+ {"foo' bar baz", 5},
+ {"foo. bar. baz beep", 10},
+ {"foo. bar, baz beep", 10},
+ }
+ for i, test := range tests {
+ out := indexFragment(test.in)
+ if test.out != out {
+ t.Errorf("test %d: expected %d, got %d", i, test.out, out)
+ }
+ }
+}
+
+func TestSplitMessage(t *testing.T) {
+ tests := []struct {
+ in string
+ sp int
+ out []string
+ }{
+ {"", 0, []string{""}},
+ {"foo", 0, []string{"foo"}},
+ {"foo bar baz beep", 0, []string{"foo bar baz beep"}},
+ {"foo bar baz beep", 15, []string{"foo bar baz ...", "beep"}},
+ {"foo bar, baz beep", 15, []string{"foo bar, ...", "baz beep"}},
+ {"0123456789012345", 0, []string{"0123456789012345"}},
+ {"0123456789012345", 15, []string{"012345678901...", "2345"}},
+ {"0123456789012345", 16, []string{"0123456789012345"}},
+ }
+ for i, test := range tests {
+ out := splitMessage(test.in, test.sp)
+ if !reflect.DeepEqual(test.out, out) {
+ t.Errorf("test %d: expected %q, got %q", i, test.out, out)
+ }
+ }
+}
+
+func TestClientCommands(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Avoid having to type ridiculously long lines to test that
+ // messages longer than SplitLen are correctly sent to the server.
+ c.cfg.SplitLen = 23
+
+ c.Pass("password")
+ s.nc.Expect("PASS password")
+
+ c.Nick("test")
+ s.nc.Expect("NICK test")
+
+ c.User("test", "Testing IRC")
+ s.nc.Expect("USER test 12 * :Testing IRC")
+
+ c.Raw("JUST a raw :line")
+ s.nc.Expect("JUST a raw :line")
+
+ c.Join("#foo")
+ s.nc.Expect("JOIN #foo")
+ c.Join("#foo bar")
+ s.nc.Expect("JOIN #foo bar")
+
+ c.Part("#foo")
+ s.nc.Expect("PART #foo")
+ c.Part("#foo", "Screw you guys...")
+ s.nc.Expect("PART #foo :Screw you guys...")
+
+ c.Quit()
+ s.nc.Expect("QUIT :GoBye!")
+ c.Quit("I'm going home.")
+ s.nc.Expect("QUIT :I'm going home.")
+
+ c.Whois("somebody")
+ s.nc.Expect("WHOIS somebody")
+
+ c.Who("*@some.host.com")
+ s.nc.Expect("WHO *@some.host.com")
+
+ c.Privmsg("#foo", "bar")
+ s.nc.Expect("PRIVMSG #foo :bar")
+
+ c.Privmsgln("#foo", "bar")
+ s.nc.Expect("PRIVMSG #foo :bar")
+
+ c.Privmsgf("#foo", "say %s", "foo")
+ s.nc.Expect("PRIVMSG #foo :say foo")
+
+ c.Privmsgln("#foo", "bar", 1, 3.54, []int{24, 36})
+ s.nc.Expect("PRIVMSG #foo :bar 1 3.54 [24 36]")
+
+ c.Privmsgf("#foo", "user %d is at %s", 2, "home")
+ s.nc.Expect("PRIVMSG #foo :user 2 is at home")
+
+ // 0123456789012345678901234567890123
+ c.Privmsg("#foo", "foo bar baz blorp. woo woobly woo.")
+ s.nc.Expect("PRIVMSG #foo :foo bar baz blorp. ...")
+ s.nc.Expect("PRIVMSG #foo :woo woobly woo.")
+
+ c.Privmsgln("#foo", "foo bar baz blorp. woo woobly woo.")
+ s.nc.Expect("PRIVMSG #foo :foo bar baz blorp. ...")
+ s.nc.Expect("PRIVMSG #foo :woo woobly woo.")
+
+ c.Privmsgf("#foo", "%s %s", "foo bar baz blorp.", "woo woobly woo.")
+ s.nc.Expect("PRIVMSG #foo :foo bar baz blorp. ...")
+ s.nc.Expect("PRIVMSG #foo :woo woobly woo.")
+
+ c.Privmsgln("#foo", "foo bar", 3.54, "blorp.", "woo", "woobly", []int{1, 2})
+ s.nc.Expect("PRIVMSG #foo :foo bar 3.54 blorp. ...")
+ s.nc.Expect("PRIVMSG #foo :woo woobly [1 2]")
+
+ c.Privmsgf("#foo", "%s %.2f %s %s %s %v", "foo bar", 3.54, "blorp.", "woo", "woobly", []int{1, 2})
+ s.nc.Expect("PRIVMSG #foo :foo bar 3.54 blorp. ...")
+ s.nc.Expect("PRIVMSG #foo :woo woobly [1 2]")
+
+ c.Notice("somebody", "something")
+ s.nc.Expect("NOTICE somebody :something")
+
+ // 01234567890123456789012345678901234567
+ c.Notice("somebody", "something much much longer that splits")
+ s.nc.Expect("NOTICE somebody :something much much ...")
+ s.nc.Expect("NOTICE somebody :longer that splits")
+
+ c.Ctcp("somebody", "ping", "123456789")
+ s.nc.Expect("PRIVMSG somebody :\001PING 123456789\001")
+
+ c.Ctcp("somebody", "ping", "123456789012345678901234567890")
+ s.nc.Expect("PRIVMSG somebody :\001PING 12345678901234567890...\001")
+ s.nc.Expect("PRIVMSG somebody :\001PING 1234567890\001")
+
+ c.CtcpReply("somebody", "pong", "123456789012345678901234567890")
+ s.nc.Expect("NOTICE somebody :\001PONG 12345678901234567890...\001")
+ s.nc.Expect("NOTICE somebody :\001PONG 1234567890\001")
+
+ c.CtcpReply("somebody", "pong", "123456789")
+ s.nc.Expect("NOTICE somebody :\001PONG 123456789\001")
+
+ c.Version("somebody")
+ s.nc.Expect("PRIVMSG somebody :\001VERSION\001")
+
+ c.Action("#foo", "pokes somebody")
+ s.nc.Expect("PRIVMSG #foo :\001ACTION pokes somebody\001")
+
+ c.Topic("#foo")
+ s.nc.Expect("TOPIC #foo")
+ c.Topic("#foo", "la la la")
+ s.nc.Expect("TOPIC #foo :la la la")
+
+ c.Mode("#foo")
+ s.nc.Expect("MODE #foo")
+ c.Mode("#foo", "+o somebody")
+ s.nc.Expect("MODE #foo +o somebody")
+
+ c.Away()
+ s.nc.Expect("AWAY")
+ c.Away("Dave's not here, man.")
+ s.nc.Expect("AWAY :Dave's not here, man.")
+
+ c.Invite("somebody", "#foo")
+ s.nc.Expect("INVITE somebody #foo")
+
+ c.Oper("user", "pass")
+ s.nc.Expect("OPER user pass")
+
+ c.VHost("user", "pass")
+ s.nc.Expect("VHOST user pass")
+}
diff --git a/vendor/github.com/fluffle/goirc/client/connection.go b/vendor/github.com/fluffle/goirc/client/connection.go
new file mode 100644
index 0000000..f84621a
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/connection.go
@@ -0,0 +1,581 @@
+package client
+
+import (
+ "bufio"
+ "crypto/tls"
+ "fmt"
+ "io"
+ "net"
+ "net/url"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/fluffle/goirc/logging"
+ "github.com/fluffle/goirc/state"
+ "golang.org/x/net/proxy"
+)
+
+// Conn encapsulates a connection to a single IRC server. Create
+// one with Client or SimpleClient.
+type Conn struct {
+ // For preventing races on (dis)connect.
+ mu sync.RWMutex
+
+ // Contains parameters that people can tweak to change client behaviour.
+ cfg *Config
+
+ // Handlers
+ intHandlers *hSet
+ fgHandlers *hSet
+ bgHandlers *hSet
+
+ // State tracker for nicks and channels
+ st state.Tracker
+ stRemovers []Remover
+
+ // I/O stuff to server
+ dialer *net.Dialer
+ proxyDialer proxy.Dialer
+ sock net.Conn
+ io *bufio.ReadWriter
+ in chan *Line
+ out chan string
+ connected bool
+
+ // Control channel and WaitGroup for goroutines
+ die chan struct{}
+ wg sync.WaitGroup
+
+ // Internal counters for flood protection
+ badness time.Duration
+ lastsent time.Time
+}
+
+// Config contains options that can be passed to Client to change the
+// behaviour of the library during use. It is recommended that NewConfig
+// is used to create this struct rather than instantiating one directly.
+// Passing a Config with no Nick in the Me field to Client will result
+// in unflattering consequences.
+type Config struct {
+ // Set this to provide the Nick, Ident and Name for the client to use.
+ // It is recommended to call Conn.Me to get up-to-date information
+ // about the current state of the client's IRC nick after connecting.
+ Me *state.Nick
+
+ // Hostname to connect to and optional connect password.
+ // Changing these after connection will have no effect until the
+ // client reconnects.
+ Server, Pass string
+
+ // Are we connecting via SSL? Do we care about certificate validity?
+ // Changing these after connection will have no effect until the
+ // client reconnects.
+ SSL bool
+ SSLConfig *tls.Config
+
+ // To connect via proxy set the proxy url here.
+ // Changing these after connection will have no effect until the
+ // client reconnects.
+ Proxy string
+
+ // Local address to bind to when connecting to the server.
+ LocalAddr string
+
+ // Replaceable function to customise the 433 handler's new nick.
+ // By default an underscore "_" is appended to the current nick.
+ NewNick func(string) string
+
+ // Client->server ping frequency, in seconds. Defaults to 3m.
+ // Set to 0 to disable client-side pings.
+ PingFreq time.Duration
+
+ // The duration before a connection timeout is triggered. Defaults to 1m.
+ // Set to 0 to wait indefinitely.
+ Timeout time.Duration
+
+ // Set this to true to disable flood protection and false to re-enable.
+ Flood bool
+
+ // Sent as the reply to a CTCP VERSION message.
+ Version string
+
+ // Sent as the default QUIT message if Quit is called with no args.
+ QuitMessage string
+
+ // Configurable panic recovery for all handlers.
+ // Defaults to logging an error, see LogPanic.
+ Recover func(*Conn, *Line)
+
+ // Split PRIVMSGs, NOTICEs and CTCPs longer than SplitLen characters
+ // over multiple lines. Default to 450 if not set.
+ SplitLen int
+}
+
+// NewConfig creates a Config struct containing sensible defaults.
+// It takes one required argument: the nick to use for the client.
+// Subsequent string arguments set the client's ident and "real"
+// name, but these are optional.
+func NewConfig(nick string, args ...string) *Config {
+ cfg := &Config{
+ Me: &state.Nick{Nick: nick},
+ PingFreq: 3 * time.Minute,
+ NewNick: func(s string) string { return s + "_" },
+ Recover: (*Conn).LogPanic, // in dispatch.go
+ SplitLen: defaultSplit,
+ Timeout: 60 * time.Second,
+ }
+ cfg.Me.Ident = "goirc"
+ if len(args) > 0 && args[0] != "" {
+ cfg.Me.Ident = args[0]
+ }
+ cfg.Me.Name = "Powered by GoIRC"
+ if len(args) > 1 && args[1] != "" {
+ cfg.Me.Name = args[1]
+ }
+ cfg.Version = "Powered by GoIRC"
+ cfg.QuitMessage = "GoBye!"
+ return cfg
+}
+
+// SimpleClient creates a new Conn, passing its arguments to NewConfig.
+// If you don't need to change any client options and just want to get
+// started quickly, this is a convenient shortcut.
+func SimpleClient(nick string, args ...string) *Conn {
+ conn := Client(NewConfig(nick, args...))
+ return conn
+}
+
+// Client takes a Config struct and returns a new Conn ready to have
+// handlers added and connect to a server.
+func Client(cfg *Config) *Conn {
+ if cfg == nil {
+ cfg = NewConfig("__idiot__")
+ }
+ if cfg.Me == nil || cfg.Me.Nick == "" || cfg.Me.Ident == "" {
+ cfg.Me = &state.Nick{Nick: "__idiot__"}
+ cfg.Me.Ident = "goirc"
+ cfg.Me.Name = "Powered by GoIRC"
+ }
+
+ dialer := new(net.Dialer)
+ dialer.Timeout = cfg.Timeout
+ if cfg.LocalAddr != "" {
+ if !hasPort(cfg.LocalAddr) {
+ cfg.LocalAddr += ":0"
+ }
+
+ local, err := net.ResolveTCPAddr("tcp", cfg.LocalAddr)
+ if err == nil {
+ dialer.LocalAddr = local
+ } else {
+ logging.Error("irc.Client(): Cannot resolve local address %s: %s", cfg.LocalAddr, err)
+ }
+ }
+
+ conn := &Conn{
+ cfg: cfg,
+ dialer: dialer,
+ intHandlers: handlerSet(),
+ fgHandlers: handlerSet(),
+ bgHandlers: handlerSet(),
+ stRemovers: make([]Remover, 0, len(stHandlers)),
+ lastsent: time.Now(),
+ }
+ conn.addIntHandlers()
+ return conn
+}
+
+// Connected returns true if the client is successfully connected to
+// an IRC server. It becomes true when the TCP connection is established,
+// and false again when the connection is closed.
+func (conn *Conn) Connected() bool {
+ conn.mu.RLock()
+ defer conn.mu.RUnlock()
+ return conn.connected
+}
+
+// Config returns a pointer to the Config struct used by the client.
+// Many of the elements of Config may be changed at any point to
+// affect client behaviour. To disable flood protection temporarily,
+// for example, a handler could do:
+//
+// conn.Config().Flood = true
+// // Send many lines to the IRC server, risking "excess flood"
+// conn.Config().Flood = false
+//
+func (conn *Conn) Config() *Config {
+ return conn.cfg
+}
+
+// Me returns a state.Nick that reflects the client's IRC nick at the
+// time it is called. If state tracking is enabled, this comes from
+// the tracker, otherwise it is equivalent to conn.cfg.Me.
+func (conn *Conn) Me() *state.Nick {
+ if conn.st != nil {
+ conn.cfg.Me = conn.st.Me()
+ }
+ return conn.cfg.Me
+}
+
+// StateTracker returns the state tracker being used by the client,
+// if tracking is enabled, and nil otherwise.
+func (conn *Conn) StateTracker() state.Tracker {
+ return conn.st
+}
+
+// EnableStateTracking causes the client to track information about
+// all channels it is joined to, and all the nicks in those channels.
+// This can be rather handy for a number of bot-writing tasks. See
+// the state package for more details.
+//
+// NOTE: Calling this while connected to an IRC server may cause the
+// state tracker to become very confused all over STDERR if logging
+// is enabled. State tracking should enabled before connecting or
+// at a pinch while the client is not joined to any channels.
+func (conn *Conn) EnableStateTracking() {
+ conn.mu.Lock()
+ defer conn.mu.Unlock()
+ if conn.st == nil {
+ n := conn.cfg.Me
+ conn.st = state.NewTracker(n.Nick)
+ conn.st.NickInfo(n.Nick, n.Ident, n.Host, n.Name)
+ conn.cfg.Me = conn.st.Me()
+ conn.addSTHandlers()
+ }
+}
+
+// DisableStateTracking causes the client to stop tracking information
+// about the channels and nicks it knows of. It will also wipe current
+// state from the state tracker.
+func (conn *Conn) DisableStateTracking() {
+ conn.mu.Lock()
+ defer conn.mu.Unlock()
+ if conn.st != nil {
+ conn.cfg.Me = conn.st.Me()
+ conn.delSTHandlers()
+ conn.st.Wipe()
+ conn.st = nil
+ }
+}
+
+// Per-connection state initialisation.
+func (conn *Conn) initialise() {
+ conn.io = nil
+ conn.sock = nil
+ conn.in = make(chan *Line, 32)
+ conn.out = make(chan string, 32)
+ conn.die = make(chan struct{})
+ if conn.st != nil {
+ conn.st.Wipe()
+ }
+}
+
+// ConnectTo connects the IRC client to "host[:port]", which should be either
+// a hostname or an IP address, with an optional port. It sets the client's
+// Config.Server to host, Config.Pass to pass if one is provided, and then
+// calls Connect.
+func (conn *Conn) ConnectTo(host string, pass ...string) error {
+ conn.cfg.Server = host
+ if len(pass) > 0 {
+ conn.cfg.Pass = pass[0]
+ }
+ return conn.Connect()
+}
+
+// Connect connects the IRC client to the server configured in Config.Server.
+// To enable explicit SSL on the connection to the IRC server, set Config.SSL
+// to true before calling Connect(). The port will default to 6697 if SSL is
+// enabled, and 6667 otherwise.
+// To enable connecting via a proxy server, set Config.Proxy to the proxy URL
+// (example socks5://localhost:9000) before calling Connect().
+//
+// Upon successful connection, Connected will return true and a REGISTER event
+// will be fired. This is mostly for internal use; it is suggested that a
+// handler for the CONNECTED event is used to perform any initial client work
+// like joining channels and sending messages.
+func (conn *Conn) Connect() error {
+ // We don't want to hold conn.mu while firing the REGISTER event,
+ // and it's much easier and less error prone to defer the unlock,
+ // so the connect mechanics have been delegated to internalConnect.
+ err := conn.internalConnect()
+ if err == nil {
+ conn.dispatch(&Line{Cmd: REGISTER, Time: time.Now()})
+ }
+ return err
+}
+
+// internalConnect handles the work of actually connecting to the server.
+func (conn *Conn) internalConnect() error {
+ conn.mu.Lock()
+ defer conn.mu.Unlock()
+ conn.initialise()
+
+ if conn.cfg.Server == "" {
+ return fmt.Errorf("irc.Connect(): cfg.Server must be non-empty")
+ }
+ if conn.connected {
+ return fmt.Errorf("irc.Connect(): Cannot connect to %s, already connected.", conn.cfg.Server)
+ }
+
+ if !hasPort(conn.cfg.Server) {
+ if conn.cfg.SSL {
+ conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6697")
+ } else {
+ conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6667")
+ }
+ }
+
+ if conn.cfg.Proxy != "" {
+ proxyURL, err := url.Parse(conn.cfg.Proxy)
+ if err != nil {
+ return err
+ }
+ conn.proxyDialer, err = proxy.FromURL(proxyURL, conn.dialer)
+ if err != nil {
+ return err
+ }
+
+ logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server)
+ if s, err := conn.proxyDialer.Dial("tcp", conn.cfg.Server); err == nil {
+ conn.sock = s
+ } else {
+ return err
+ }
+ } else {
+ logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server)
+ if s, err := conn.dialer.Dial("tcp", conn.cfg.Server); err == nil {
+ conn.sock = s
+ } else {
+ return err
+ }
+ }
+
+ if conn.cfg.SSL {
+ logging.Info("irc.Connect(): Performing SSL handshake.")
+ s := tls.Client(conn.sock, conn.cfg.SSLConfig)
+ if err := s.Handshake(); err != nil {
+ return err
+ }
+ conn.sock = s
+ }
+
+ conn.postConnect(true)
+ conn.connected = true
+ return nil
+}
+
+// postConnect performs post-connection setup, for ease of testing.
+func (conn *Conn) postConnect(start bool) {
+ conn.io = bufio.NewReadWriter(
+ bufio.NewReader(conn.sock),
+ bufio.NewWriter(conn.sock))
+ if start {
+ conn.wg.Add(3)
+ go conn.send()
+ go conn.recv()
+ go conn.runLoop()
+ if conn.cfg.PingFreq > 0 {
+ conn.wg.Add(1)
+ go conn.ping()
+ }
+ }
+}
+
+// hasPort returns true if the string hostname has a :port suffix.
+// It was copied from net/http for great justice.
+func hasPort(s string) bool {
+ return strings.LastIndex(s, ":") > strings.LastIndex(s, "]")
+}
+
+// send is started as a goroutine after a connection is established.
+// It shuttles data from the output channel to write(), and is killed
+// when Conn.die is closed.
+func (conn *Conn) send() {
+ for {
+ select {
+ case line := <-conn.out:
+ if err := conn.write(line); err != nil {
+ logging.Error("irc.send(): %s", err.Error())
+ // We can't defer this, because Close() waits for it.
+ conn.wg.Done()
+ conn.Close()
+ return
+ }
+ case <-conn.die:
+ // control channel closed, bail out
+ conn.wg.Done()
+ return
+ }
+ }
+}
+
+// recv is started as a goroutine after a connection is established.
+// It receives "\r\n" terminated lines from the server, parses them into
+// Lines, and sends them to the input channel.
+func (conn *Conn) recv() {
+ for {
+ s, err := conn.io.ReadString('\n')
+ if err != nil {
+ if err != io.EOF {
+ logging.Error("irc.recv(): %s", err.Error())
+ }
+ // We can't defer this, because Close() waits for it.
+ conn.wg.Done()
+ conn.Close()
+ return
+ }
+ s = strings.Trim(s, "\r\n")
+ logging.Debug("<- %s", s)
+
+ if line := ParseLine(s); line != nil {
+ line.Time = time.Now()
+ conn.in <- line
+ } else {
+ logging.Warn("irc.recv(): problems parsing line:\n %s", s)
+ }
+ }
+}
+
+// ping is started as a goroutine after a connection is established, as
+// long as Config.PingFreq >0. It pings the server every PingFreq seconds.
+func (conn *Conn) ping() {
+ defer conn.wg.Done()
+ tick := time.NewTicker(conn.cfg.PingFreq)
+ for {
+ select {
+ case <-tick.C:
+ conn.Ping(fmt.Sprintf("%d", time.Now().UnixNano()))
+ case <-conn.die:
+ // control channel closed, bail out
+ tick.Stop()
+ return
+ }
+ }
+}
+
+// runLoop is started as a goroutine after a connection is established.
+// It pulls Lines from the input channel and dispatches them to any
+// handlers that have been registered for that IRC verb.
+func (conn *Conn) runLoop() {
+ defer conn.wg.Done()
+ for {
+ select {
+ case line := <-conn.in:
+ conn.dispatch(line)
+ case <-conn.die:
+ // control channel closed, bail out
+ return
+ }
+ }
+}
+
+// write writes a \r\n terminated line of output to the connected server,
+// using Hybrid's algorithm to rate limit if conn.cfg.Flood is false.
+func (conn *Conn) write(line string) error {
+ if !conn.cfg.Flood {
+ if t := conn.rateLimit(len(line)); t != 0 {
+ // sleep for the current line's time value before sending it
+ logging.Info("irc.rateLimit(): Flood! Sleeping for %.2f secs.",
+ t.Seconds())
+ <-time.After(t)
+ }
+ }
+
+ if _, err := conn.io.WriteString(line + "\r\n"); err != nil {
+ return err
+ }
+ if err := conn.io.Flush(); err != nil {
+ return err
+ }
+ if strings.HasPrefix(line, "PASS") {
+ line = "PASS **************"
+ }
+ logging.Debug("-> %s", line)
+ return nil
+}
+
+// rateLimit implements Hybrid's flood control algorithm for outgoing lines.
+func (conn *Conn) rateLimit(chars int) time.Duration {
+ // Hybrid's algorithm allows for 2 seconds per line and an additional
+ // 1/120 of a second per character on that line.
+ linetime := 2*time.Second + time.Duration(chars)*time.Second/120
+ elapsed := time.Now().Sub(conn.lastsent)
+ if conn.badness += linetime - elapsed; conn.badness < 0 {
+ // negative badness times are badness...
+ conn.badness = 0
+ }
+ conn.lastsent = time.Now()
+ // If we've sent more than 10 second's worth of lines according to the
+ // calculation above, then we're at risk of "Excess Flood".
+ if conn.badness > 10*time.Second {
+ return linetime
+ }
+ return 0
+}
+
+// Close tears down all connection-related state. It is called when either
+// the sending or receiving goroutines encounter an error.
+// It may also be used to forcibly shut down the connection to the server.
+func (conn *Conn) Close() error {
+ // Guard against double-call of Close() if we get an error in send()
+ // as calling sock.Close() will cause recv() to receive EOF in readstring()
+ conn.mu.Lock()
+ if !conn.connected {
+ conn.mu.Unlock()
+ return nil
+ }
+ logging.Info("irc.Close(): Disconnected from server.")
+ conn.connected = false
+ err := conn.sock.Close()
+ close(conn.die)
+ // Drain both in and out channels to avoid a deadlock if the buffers
+ // have filled. See TestSendDeadlockOnFullBuffer in connection_test.go.
+ conn.drainIn()
+ conn.drainOut()
+ conn.wg.Wait()
+ conn.mu.Unlock()
+ // Dispatch after closing connection but before reinit
+ // so event handlers can still access state information.
+ conn.dispatch(&Line{Cmd: DISCONNECTED, Time: time.Now()})
+ return err
+}
+
+// drainIn sends all data buffered in conn.in to /dev/null.
+func (conn *Conn) drainIn() {
+ for {
+ select {
+ case <-conn.in:
+ default:
+ return
+ }
+ }
+}
+
+// drainOut does the same for conn.out. Generics!
+func (conn *Conn) drainOut() {
+ for {
+ select {
+ case <-conn.out:
+ default:
+ return
+ }
+ }
+}
+
+// Dumps a load of information about the current state of the connection to a
+// string for debugging state tracking and other such things.
+func (conn *Conn) String() string {
+ str := "GoIRC Connection\n"
+ str += "----------------\n\n"
+ if conn.Connected() {
+ str += "Connected to " + conn.cfg.Server + "\n\n"
+ } else {
+ str += "Not currently connected!\n\n"
+ }
+ str += conn.Me().String() + "\n"
+ if conn.st != nil {
+ str += conn.st.String() + "\n"
+ }
+ return str
+}
diff --git a/vendor/github.com/fluffle/goirc/client/connection_test.go b/vendor/github.com/fluffle/goirc/client/connection_test.go
new file mode 100644
index 0000000..acf4713
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/connection_test.go
@@ -0,0 +1,585 @@
+package client
+
+import (
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/fluffle/goirc/state"
+ "github.com/golang/mock/gomock"
+)
+
+type checker struct {
+ t *testing.T
+ c chan struct{}
+}
+
+func callCheck(t *testing.T) checker {
+ return checker{t: t, c: make(chan struct{})}
+}
+
+func (c checker) call() {
+ c.c <- struct{}{}
+}
+
+func (c checker) assertNotCalled(fmt string, args ...interface{}) {
+ select {
+ case <-c.c:
+ c.t.Errorf(fmt, args...)
+ default:
+ }
+}
+
+func (c checker) assertWasCalled(fmt string, args ...interface{}) {
+ select {
+ case <-c.c:
+ case <-time.After(time.Millisecond):
+ // Usually need to wait for goroutines to settle :-/
+ c.t.Errorf(fmt, args...)
+ }
+}
+
+type testState struct {
+ ctrl *gomock.Controller
+ st *state.MockTracker
+ nc *mockNetConn
+ c *Conn
+}
+
+// NOTE: including a second argument at all prevents calling c.postConnect()
+func setUp(t *testing.T, start ...bool) (*Conn, *testState) {
+ ctrl := gomock.NewController(t)
+ st := state.NewMockTracker(ctrl)
+ nc := MockNetConn(t)
+ c := SimpleClient("test", "test", "Testing IRC")
+ c.initialise()
+
+ c.st = st
+ c.sock = nc
+ c.cfg.Flood = true // Tests can take a while otherwise
+ c.connected = true
+ // If a second argument is passed to setUp, we tell postConnect not to
+ // start the various goroutines that shuttle data around.
+ c.postConnect(len(start) == 0)
+ // Sleep 1ms to allow background routines to start.
+ <-time.After(time.Millisecond)
+
+ return c, &testState{ctrl, st, nc, c}
+}
+
+func (s *testState) tearDown() {
+ s.nc.ExpectNothing()
+ s.c.Close()
+ s.ctrl.Finish()
+}
+
+// Practically the same as the above test, but Close is called implicitly
+// by recv() getting an EOF from the mock connection.
+func TestEOF(t *testing.T) {
+ c, s := setUp(t)
+ // Since we're not using tearDown() here, manually call Finish()
+ defer s.ctrl.Finish()
+
+ // Set up a handler to detect whether disconnected handlers are called
+ dcon := callCheck(t)
+ c.HandleFunc(DISCONNECTED, func(conn *Conn, line *Line) {
+ dcon.call()
+ })
+
+ // Simulate EOF from server
+ s.nc.Close()
+
+ // Verify that disconnected handler was called
+ dcon.assertWasCalled("Conn did not call disconnected handlers.")
+
+ // Verify that the connection no longer thinks it's connected
+ if c.Connected() {
+ t.Errorf("Conn still thinks it's connected to the server.")
+ }
+}
+
+func TestClientAndStateTracking(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ st := state.NewMockTracker(ctrl)
+ c := SimpleClient("test", "test", "Testing IRC")
+
+ // Assert some basic things about the initial state of the Conn struct
+ me := c.cfg.Me
+ if me.Nick != "test" || me.Ident != "test" ||
+ me.Name != "Testing IRC" || me.Host != "" {
+ t.Errorf("Conn.cfg.Me not correctly initialised.")
+ }
+ // Check that the internal handlers are correctly set up
+ for k, _ := range intHandlers {
+ if _, ok := c.intHandlers.set[strings.ToLower(k)]; !ok {
+ t.Errorf("Missing internal handler for '%s'.", k)
+ }
+ }
+
+ // Now enable the state tracking code and check its handlers
+ c.EnableStateTracking()
+ for k, _ := range stHandlers {
+ if _, ok := c.intHandlers.set[strings.ToLower(k)]; !ok {
+ t.Errorf("Missing state handler for '%s'.", k)
+ }
+ }
+ if len(c.stRemovers) != len(stHandlers) {
+ t.Errorf("Incorrect number of Removers (%d != %d) when adding state handlers.",
+ len(c.stRemovers), len(stHandlers))
+ }
+ if neu := c.Me(); neu.Nick != me.Nick || neu.Ident != me.Ident ||
+ neu.Name != me.Name || neu.Host != me.Host {
+ t.Errorf("Enabling state tracking erased information about me!")
+ }
+
+ // We're expecting the untracked me to be replaced by a tracked one
+ if c.st == nil {
+ t.Errorf("State tracker not enabled correctly.")
+ }
+ if me = c.cfg.Me; me.Nick != "test" || me.Ident != "test" ||
+ me.Name != "Testing IRC" || me.Host != "" {
+ t.Errorf("Enabling state tracking did not replace Me correctly.")
+ }
+
+ // Now, shim in the mock state tracker and test disabling state tracking
+ c.st = st
+ gomock.InOrder(
+ st.EXPECT().Me().Return(me),
+ st.EXPECT().Wipe(),
+ )
+ c.DisableStateTracking()
+ if c.st != nil || !c.cfg.Me.Equals(me) {
+ t.Errorf("State tracker not disabled correctly.")
+ }
+
+ // Finally, check state tracking handlers were all removed correctly
+ for k, _ := range stHandlers {
+ if _, ok := c.intHandlers.set[strings.ToLower(k)]; ok && k != "NICK" {
+ // A bit leaky, because intHandlers adds a NICK handler.
+ t.Errorf("State handler for '%s' not removed correctly.", k)
+ }
+ }
+ if len(c.stRemovers) != 0 {
+ t.Errorf("stRemovers not zeroed correctly when removing state handlers.")
+ }
+ ctrl.Finish()
+}
+
+func TestSendExitsOnDie(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ defer s.tearDown()
+
+ // Assert that before send is running, nothing should be sent to the socket
+ // but writes to the buffered channel "out" should not block.
+ c.out <- "SENT BEFORE START"
+ s.nc.ExpectNothing()
+
+ // We want to test that the a goroutine calling send will exit correctly.
+ exited := callCheck(t)
+ // send() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(1)
+ go func() {
+ c.send()
+ exited.call()
+ }()
+
+ // send is now running in the background as if started by postConnect.
+ // This should read the line previously buffered in c.out, and write it
+ // to the socket connection.
+ s.nc.Expect("SENT BEFORE START")
+
+ // Send another line, just to be sure :-)
+ c.out <- "SENT AFTER START"
+ s.nc.Expect("SENT AFTER START")
+
+ // Now, use the control channel to exit send and kill the goroutine.
+ // This sneakily uses the fact that the other two goroutines that would
+ // normally be waiting for die to close are not running, so we only send
+ // to the goroutine started above. Normally Close() closes c.die and
+ // signals to all three goroutines (send, ping, runLoop) to exit.
+ exited.assertNotCalled("Exited before signal sent.")
+ c.die <- struct{}{}
+ exited.assertWasCalled("Didn't exit after signal.")
+ s.nc.ExpectNothing()
+
+ // Sending more on c.out shouldn't reach the network.
+ c.out <- "SENT AFTER END"
+ s.nc.ExpectNothing()
+}
+
+func TestSendExitsOnWriteError(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ // We can't use tearDown here because we're testing shutdown conditions
+ // (and so need to EXPECT() a call to st.Wipe() in the right place)
+ defer s.ctrl.Finish()
+
+ // We want to test that the a goroutine calling send will exit correctly.
+ exited := callCheck(t)
+ // send() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(1)
+ go func() {
+ c.send()
+ exited.call()
+ }()
+
+ // Send a line to be sure things are good.
+ c.out <- "SENT AFTER START"
+ s.nc.Expect("SENT AFTER START")
+
+ // Now, close the underlying socket to cause write() to return an error.
+ // This will call Close() => a call to st.Wipe() will happen.
+ exited.assertNotCalled("Exited before signal sent.")
+ s.nc.Close()
+ // Sending more on c.out shouldn't reach the network, but we need to send
+ // *something* to trigger a call to write() that will fail.
+ c.out <- "SENT AFTER END"
+ exited.assertWasCalled("Didn't exit after signal.")
+ s.nc.ExpectNothing()
+}
+
+func TestSendDeadlockOnFullBuffer(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ // We can't use tearDown here because we're testing a deadlock condition
+ // and if tearDown tries to call Close() it will deadlock some more
+ // because send() is holding the conn mutex via Close() already.
+ defer s.ctrl.Finish()
+
+ // We want to test that the a goroutine calling send will exit correctly.
+ loopExit := callCheck(t)
+ sendExit := callCheck(t)
+ // send() and runLoop() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(2)
+
+ // The deadlock arises when a handler being called from conn.dispatch() in
+ // runLoop() tries to write to conn.out to send a message back to the IRC
+ // server, but the buffer is full. If at the same time send() is
+ // calling conn.Close() and waiting in there for runLoop() to call
+ // conn.wg.Done(), it will not empty the buffer of conn.out => deadlock.
+ //
+ // We simulate this by artifically filling conn.out. We must use a
+ // goroutine to put in one more line than the buffer can hold, because
+ // send() will read a line from conn.out on its first loop iteration:
+ go func() {
+ for i := 0; i < 33; i++ {
+ c.out <- "FILL BUFFER WITH CRAP"
+ }
+ }()
+ // Then we add a handler that tries to write a line to conn.out:
+ c.HandleFunc(PRIVMSG, func(conn *Conn, line *Line) {
+ conn.Raw(line.Raw)
+ })
+ // And trigger it by starting runLoop and inserting a line into conn.in:
+ go func() {
+ c.runLoop()
+ loopExit.call()
+ }()
+ c.in <- &Line{Cmd: PRIVMSG, Raw: "WRITE THAT CAUSES DEADLOCK"}
+
+ // At this point the handler should be blocked on a write to conn.out,
+ // preventing runLoop from looping and thus noticing conn.die is closed.
+ //
+ // The next part is to force send() to call conn.Close(), which can
+ // be done by closing the fake net.Conn so that it returns an error on
+ // calls to Write():
+ s.nc.ExpectNothing()
+ s.nc.Close()
+
+ // Now when send is started it will read one line from conn.out and try
+ // to write it to the socket. It should immediately receive an error and
+ // call conn.Close(), triggering the deadlock as it waits forever for
+ // runLoop to call conn.wg.Done.
+ go func() {
+ c.send()
+ sendExit.call()
+ }()
+
+ // Make sure that things are definitely deadlocked.
+ <-time.After(time.Millisecond)
+
+ // Verify that the connection no longer thinks it's connected, i.e.
+ // conn.Close() has definitely been called. We can't call
+ // conn.Connected() here because conn.Close() holds the mutex.
+ if c.connected {
+ t.Errorf("Conn still thinks it's connected to the server.")
+ }
+
+ // We expect both loops to terminate cleanly. If either of them don't
+ // then we have successfully deadlocked :-(
+ loopExit.assertWasCalled("runLoop did not exit cleanly.")
+ sendExit.assertWasCalled("send did not exit cleanly.")
+}
+
+func TestRecv(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ // We can't use tearDown here because we're testing shutdown conditions
+ // (and so need to EXPECT() a call to st.Wipe() in the right place)
+ defer s.ctrl.Finish()
+
+ // Send a line before recv is started up, to verify nothing appears on c.in
+ s.nc.Send(":irc.server.org 001 test :First test line.")
+
+ // reader is a helper to do a "non-blocking" read of c.in
+ reader := func() *Line {
+ select {
+ case <-time.After(time.Millisecond):
+ case l := <-c.in:
+ return l
+ }
+ return nil
+ }
+ if l := reader(); l != nil {
+ t.Errorf("Line parsed before recv started.")
+ }
+
+ // We want to test that the a goroutine calling recv will exit correctly.
+ exited := callCheck(t)
+ // recv() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(1)
+ go func() {
+ c.recv()
+ exited.call()
+ }()
+
+ // Now, this should mean that we'll receive our parsed line on c.in
+ if l := reader(); l == nil || l.Cmd != "001" {
+ t.Errorf("Bad first line received on input channel")
+ }
+
+ // Send a second line, just to be sure.
+ s.nc.Send(":irc.server.org 002 test :Second test line.")
+ if l := reader(); l == nil || l.Cmd != "002" {
+ t.Errorf("Bad second line received on input channel.")
+ }
+
+ // Test that recv does something useful with a line it can't parse
+ // (not that there are many, ParseLine is forgiving).
+ s.nc.Send(":textwithnospaces")
+ if l := reader(); l != nil {
+ t.Errorf("Bad line still caused receive on input channel.")
+ }
+
+ // The only way recv() exits is when the socket closes.
+ exited.assertNotCalled("Exited before socket close.")
+ s.nc.Close()
+ exited.assertWasCalled("Didn't exit on socket close.")
+
+ // Since s.nc is closed we can't attempt another send on it...
+ if l := reader(); l != nil {
+ t.Errorf("Line received on input channel after socket close.")
+ }
+}
+
+func TestPing(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ defer s.tearDown()
+
+ res := time.Millisecond
+
+ // Windows has a timer resolution of 15.625ms by default.
+ // This means the test will be slower on windows, but
+ // should at least stop most of the flakiness...
+ // https://github.com/fluffle/goirc/issues/88
+ if runtime.GOOS == "windows" {
+ res = 15625 * time.Microsecond
+ }
+
+ // Set a low ping frequency for testing.
+ c.cfg.PingFreq = 10 * res
+
+ // reader is a helper to do a "non-blocking" read of c.out
+ reader := func() string {
+ select {
+ case <-time.After(res):
+ case s := <-c.out:
+ return s
+ }
+ return ""
+ }
+ if s := reader(); s != "" {
+ t.Errorf("Line output before ping started.")
+ }
+
+ // Start ping loop.
+ exited := callCheck(t)
+ // ping() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(1)
+ go func() {
+ c.ping()
+ exited.call()
+ }()
+
+ // The first ping should be after 10*res ms,
+ // so we don't expect anything now on c.in
+ if s := reader(); s != "" {
+ t.Errorf("Line output directly after ping started.")
+ }
+
+ <-time.After(c.cfg.PingFreq)
+ if s := reader(); s == "" || !strings.HasPrefix(s, "PING :") {
+ t.Errorf("Line not output after %s.", c.cfg.PingFreq)
+ }
+
+ // Reader waits for res ms and we call it a few times above.
+ <-time.After(7 * res)
+ if s := reader(); s != "" {
+ t.Errorf("Line output <%s after last ping.", 7*res)
+ }
+
+ // This is a short window in which the ping should happen
+ // This may result in flaky tests; sorry (and file a bug) if so.
+ <-time.After(2 * res)
+ if s := reader(); s == "" || !strings.HasPrefix(s, "PING :") {
+ t.Errorf("Line not output after another %s.", 2*res)
+ }
+
+ // Now kill the ping loop.
+ // This sneakily uses the fact that the other two goroutines that would
+ // normally be waiting for die to close are not running, so we only send
+ // to the goroutine started above. Normally Close() closes c.die and
+ // signals to all three goroutines (send, ping, runLoop) to exit.
+ exited.assertNotCalled("Exited before signal sent.")
+ c.die <- struct{}{}
+ exited.assertWasCalled("Didn't exit after signal.")
+ // Make sure we're no longer pinging by waiting >2x PingFreq
+ <-time.After(2*c.cfg.PingFreq + res)
+ if s := reader(); s != "" {
+ t.Errorf("Line output after ping stopped.")
+ }
+}
+
+func TestRunLoop(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ defer s.tearDown()
+
+ // Set up a handler to detect whether 001 handler is called
+ h001 := callCheck(t)
+ c.HandleFunc("001", func(conn *Conn, line *Line) {
+ h001.call()
+ })
+ h002 := callCheck(t)
+ // Set up a handler to detect whether 002 handler is called
+ c.HandleFunc("002", func(conn *Conn, line *Line) {
+ h002.call()
+ })
+
+ l1 := ParseLine(":irc.server.org 001 test :First test line.")
+ c.in <- l1
+ h001.assertNotCalled("001 handler called before runLoop started.")
+
+ // We want to test that the a goroutine calling runLoop will exit correctly.
+ // Now, we can expect the call to Dispatch to take place as runLoop starts.
+ exited := callCheck(t)
+ // runLoop() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(1)
+ go func() {
+ c.runLoop()
+ exited.call()
+ }()
+ h001.assertWasCalled("001 handler not called after runLoop started.")
+
+ // Send another line, just to be sure :-)
+ h002.assertNotCalled("002 handler called before expected.")
+ l2 := ParseLine(":irc.server.org 002 test :Second test line.")
+ c.in <- l2
+ h002.assertWasCalled("002 handler not called while runLoop started.")
+
+ // Now, use the control channel to exit send and kill the goroutine.
+ // This sneakily uses the fact that the other two goroutines that would
+ // normally be waiting for die to close are not running, so we only send
+ // to the goroutine started above. Normally Close() closes c.die and
+ // signals to all three goroutines (send, ping, runLoop) to exit.
+ exited.assertNotCalled("Exited before signal sent.")
+ c.die <- struct{}{}
+ exited.assertWasCalled("Didn't exit after signal.")
+
+ // Sending more on c.in shouldn't dispatch any further events
+ c.in <- l1
+ h001.assertNotCalled("001 handler called after runLoop ended.")
+}
+
+func TestWrite(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ // We can't use tearDown here because we're testing shutdown conditions
+ // (and so need to EXPECT() a call to st.Wipe() in the right place)
+ defer s.ctrl.Finish()
+
+ // Write should just write a line to the socket.
+ if err := c.write("yo momma"); err != nil {
+ t.Errorf("Write returned unexpected error %v", err)
+ }
+ s.nc.Expect("yo momma")
+
+ // Flood control is disabled -- setUp sets c.cfg.Flood = true -- so we should
+ // not have set c.badness at this point.
+ if c.badness != 0 {
+ t.Errorf("Flood control used when Flood = true.")
+ }
+
+ c.cfg.Flood = false
+ if err := c.write("she so useless"); err != nil {
+ t.Errorf("Write returned unexpected error %v", err)
+ }
+ s.nc.Expect("she so useless")
+
+ // The lastsent time should have been updated very recently...
+ if time.Now().Sub(c.lastsent) > time.Millisecond {
+ t.Errorf("Flood control not used when Flood = false.")
+ }
+
+ // Finally, test the error state by closing the socket then writing.
+ s.nc.Close()
+ if err := c.write("she can't pass unit tests"); err == nil {
+ t.Errorf("Expected write to return error after socket close.")
+ }
+}
+
+func TestRateLimit(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ if c.badness != 0 {
+ t.Errorf("Bad initial values for rate limit variables.")
+ }
+
+ // We'll be needing this later...
+ abs := func(i time.Duration) time.Duration {
+ if i < 0 {
+ return -i
+ }
+ return i
+ }
+
+ // Since the changes to the time module, c.lastsent is now a time.Time.
+ // It's initialised on client creation to time.Now() which for the purposes
+ // of this test was probably around 1.2 ms ago. This is inconvenient.
+ // Making it >10s ago effectively clears out the inconsistency, as this
+ // makes elapsed > linetime and thus zeros c.badness and resets c.lastsent.
+ c.lastsent = time.Now().Add(-10 * time.Second)
+ if l := c.rateLimit(60); l != 0 || c.badness != 0 {
+ t.Errorf("Rate limit got non-zero badness from long-ago lastsent.")
+ }
+
+ // So, time at the nanosecond resolution is a bit of a bitch. Choosing 60
+ // characters as the line length means we should be increasing badness by
+ // 2.5 seconds minus the delta between the two ratelimit calls. This should
+ // be minimal but it's guaranteed that it won't be zero. Use 20us as a fuzz.
+ if l := c.rateLimit(60); l != 0 ||
+ abs(c.badness-2500*time.Millisecond) > 20*time.Microsecond {
+ t.Errorf("Rate limit calculating badness incorrectly.")
+ }
+ // At this point, we can tip over the badness scale, with a bit of help.
+ // 720 chars => +8 seconds of badness => 10.5 seconds => ratelimit
+ if l := c.rateLimit(720); l != 8*time.Second ||
+ abs(c.badness-10500*time.Millisecond) > 20*time.Microsecond {
+ t.Errorf("Rate limit failed to return correct limiting values.")
+ t.Errorf("l=%d, badness=%d", l, c.badness)
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/dispatch.go b/vendor/github.com/fluffle/goirc/client/dispatch.go
new file mode 100644
index 0000000..4a5deb9
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/dispatch.go
@@ -0,0 +1,202 @@
+package client
+
+import (
+ "runtime"
+ "strings"
+ "sync"
+
+ "github.com/fluffle/goirc/logging"
+)
+
+// Handlers are triggered on incoming Lines from the server, with the handler
+// "name" being equivalent to Line.Cmd. Read the RFCs for details on what
+// replies could come from the server. They'll generally be things like
+// "PRIVMSG", "JOIN", etc. but all the numeric replies are left as ascii
+// strings of digits like "332" (mainly because I really didn't feel like
+// putting massive constant tables in).
+//
+// Foreground handlers have a guarantee of protocol consistency: all the
+// handlers for one event will have finished before the handlers for the
+// next start processing. They are run in parallel but block the event
+// loop, so care should be taken to ensure these handlers are quick :-)
+//
+// Background handlers are run in parallel and do not block the event loop.
+// This is useful for things that may need to do significant work.
+type Handler interface {
+ Handle(*Conn, *Line)
+}
+
+// Removers allow for a handler that has been previously added to the client
+// to be removed.
+type Remover interface {
+ Remove()
+}
+
+// HandlerFunc allows a bare function with this signature to implement the
+// Handler interface. It is used by Conn.HandleFunc.
+type HandlerFunc func(*Conn, *Line)
+
+func (hf HandlerFunc) Handle(conn *Conn, line *Line) {
+ hf(conn, line)
+}
+
+// Handlers are organised using a map of linked-lists, with each map
+// key representing an IRC verb or numeric, and the linked list values
+// being handlers that are executed in parallel when a Line from the
+// server with that verb or numeric arrives.
+type hSet struct {
+ set map[string]*hList
+ sync.RWMutex
+}
+
+type hList struct {
+ start, end *hNode
+}
+
+// Storing the forward and backward links in the node allows O(1) removal.
+// This probably isn't strictly necessary but I think it's kinda nice.
+type hNode struct {
+ next, prev *hNode
+ set *hSet
+ event string
+ handler Handler
+}
+
+// A hNode implements both Handler (with configurable panic recovery)...
+func (hn *hNode) Handle(conn *Conn, line *Line) {
+ defer conn.cfg.Recover(conn, line)
+ hn.handler.Handle(conn, line)
+}
+
+// ... and Remover.
+func (hn *hNode) Remove() {
+ hn.set.remove(hn)
+}
+
+func handlerSet() *hSet {
+ return &hSet{set: make(map[string]*hList)}
+}
+
+// When a new Handler is added for an event, it is wrapped in a hNode and
+// returned as a Remover so the caller can remove it at a later time.
+func (hs *hSet) add(ev string, h Handler) Remover {
+ hs.Lock()
+ defer hs.Unlock()
+ ev = strings.ToLower(ev)
+ l, ok := hs.set[ev]
+ if !ok {
+ l = &hList{}
+ }
+ hn := &hNode{
+ set: hs,
+ event: ev,
+ handler: h,
+ }
+ if !ok {
+ l.start = hn
+ } else {
+ hn.prev = l.end
+ l.end.next = hn
+ }
+ l.end = hn
+ hs.set[ev] = l
+ return hn
+}
+
+func (hs *hSet) remove(hn *hNode) {
+ hs.Lock()
+ defer hs.Unlock()
+ l, ok := hs.set[hn.event]
+ if !ok {
+ logging.Error("Removing node for unknown event '%s'", hn.event)
+ return
+ }
+ if hn.next == nil {
+ l.end = hn.prev
+ } else {
+ hn.next.prev = hn.prev
+ }
+ if hn.prev == nil {
+ l.start = hn.next
+ } else {
+ hn.prev.next = hn.next
+ }
+ hn.next = nil
+ hn.prev = nil
+ hn.set = nil
+ if l.start == nil || l.end == nil {
+ delete(hs.set, hn.event)
+ }
+}
+
+func (hs *hSet) getHandlers(ev string) []*hNode {
+ hs.RLock()
+ defer hs.RUnlock()
+ list, ok := hs.set[ev]
+ if !ok {
+ return nil
+ }
+ // Copy current list of handlers to a temporary slice under the lock.
+ handlers := make([]*hNode, 0)
+ for hn := list.start; hn != nil; hn = hn.next {
+ handlers = append(handlers, hn)
+ }
+ return handlers
+}
+
+func (hs *hSet) dispatch(conn *Conn, line *Line) {
+ ev := strings.ToLower(line.Cmd)
+ wg := &sync.WaitGroup{}
+ for _, hn := range hs.getHandlers(ev) {
+ wg.Add(1)
+ go func(hn *hNode) {
+ hn.Handle(conn, line.Copy())
+ wg.Done()
+ }(hn)
+ }
+ wg.Wait()
+}
+
+// Handle adds the provided handler to the foreground set for the named event.
+// It will return a Remover that allows that handler to be removed again.
+func (conn *Conn) Handle(name string, h Handler) Remover {
+ return conn.fgHandlers.add(name, h)
+}
+
+// HandleBG adds the provided handler to the background set for the named
+// event. It may go away in the future.
+// It will return a Remover that allows that handler to be removed again.
+func (conn *Conn) HandleBG(name string, h Handler) Remover {
+ return conn.bgHandlers.add(name, h)
+}
+
+func (conn *Conn) handle(name string, h Handler) Remover {
+ return conn.intHandlers.add(name, h)
+}
+
+// HandleFunc adds the provided function as a handler in the foreground set
+// for the named event.
+// It will return a Remover that allows that handler to be removed again.
+func (conn *Conn) HandleFunc(name string, hf HandlerFunc) Remover {
+ return conn.Handle(name, hf)
+}
+
+func (conn *Conn) dispatch(line *Line) {
+ // We run the internal handlers first, including all state tracking ones.
+ // This ensures that user-supplied handlers that use the tracker have a
+ // consistent view of the connection state in handlers that mutate it.
+ conn.intHandlers.dispatch(conn, line)
+ go conn.bgHandlers.dispatch(conn, line)
+ conn.fgHandlers.dispatch(conn, line)
+}
+
+// LogPanic is used as the default panic catcher for the client. If, like me,
+// you are not good with computer, and you'd prefer your bot not to vanish into
+// the ether whenever you make unfortunate programming mistakes, you may find
+// this useful: it will recover panics from handler code and log the errors.
+func (conn *Conn) LogPanic(line *Line) {
+ if err := recover(); err != nil {
+ _, f, l, _ := runtime.Caller(2)
+ logging.Error("%s:%d: panic: %v", f, l, err)
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/dispatch_test.go b/vendor/github.com/fluffle/goirc/client/dispatch_test.go
new file mode 100644
index 0000000..b79df64
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/dispatch_test.go
@@ -0,0 +1,201 @@
+package client
+
+import (
+ "sync/atomic"
+ "testing"
+ "time"
+)
+
+func TestHandlerSet(t *testing.T) {
+ // A Conn is needed here because the previous behaviour of passing nil to
+ // hset.dispatch causes a nil pointer dereference with panic recovery.
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ hs := handlerSet()
+ if len(hs.set) != 0 {
+ t.Errorf("New set contains things!")
+ }
+
+ callcount := new(int32)
+ f := func(_ *Conn, _ *Line) {
+ atomic.AddInt32(callcount, 1)
+ }
+
+ // Add one
+ hn1 := hs.add("ONE", HandlerFunc(f)).(*hNode)
+ hl, ok := hs.set["one"]
+ if len(hs.set) != 1 || !ok {
+ t.Errorf("Set doesn't contain 'one' list after add().")
+ }
+ if hn1.set != hs || hn1.event != "one" || hn1.prev != nil || hn1.next != nil {
+ t.Errorf("First node for 'one' not created correctly")
+ }
+ if hl.start != hn1 || hl.end != hn1 {
+ t.Errorf("Node not added to empty 'one' list correctly.")
+ }
+
+ // Add another one...
+ hn2 := hs.add("one", HandlerFunc(f)).(*hNode)
+ if len(hs.set) != 1 {
+ t.Errorf("Set contains more than 'one' list after add().")
+ }
+ if hn2.set != hs || hn2.event != "one" {
+ t.Errorf("Second node for 'one' not created correctly")
+ }
+ if hn1.prev != nil || hn1.next != hn2 || hn2.prev != hn1 || hn2.next != nil {
+ t.Errorf("Nodes for 'one' not linked correctly.")
+ }
+ if hl.start != hn1 || hl.end != hn2 {
+ t.Errorf("Node not appended to 'one' list correctly.")
+ }
+
+ // Add a third one!
+ hn3 := hs.add("one", HandlerFunc(f)).(*hNode)
+ if len(hs.set) != 1 {
+ t.Errorf("Set contains more than 'one' list after add().")
+ }
+ if hn3.set != hs || hn3.event != "one" {
+ t.Errorf("Third node for 'one' not created correctly")
+ }
+ if hn1.prev != nil || hn1.next != hn2 ||
+ hn2.prev != hn1 || hn2.next != hn3 ||
+ hn3.prev != hn2 || hn3.next != nil {
+ t.Errorf("Nodes for 'one' not linked correctly.")
+ }
+ if hl.start != hn1 || hl.end != hn3 {
+ t.Errorf("Node not appended to 'one' list correctly.")
+ }
+
+ // And finally a fourth one!
+ hn4 := hs.add("one", HandlerFunc(f)).(*hNode)
+ if len(hs.set) != 1 {
+ t.Errorf("Set contains more than 'one' list after add().")
+ }
+ if hn4.set != hs || hn4.event != "one" {
+ t.Errorf("Fourth node for 'one' not created correctly.")
+ }
+ if hn1.prev != nil || hn1.next != hn2 ||
+ hn2.prev != hn1 || hn2.next != hn3 ||
+ hn3.prev != hn2 || hn3.next != hn4 ||
+ hn4.prev != hn3 || hn4.next != nil {
+ t.Errorf("Nodes for 'one' not linked correctly.")
+ }
+ if hl.start != hn1 || hl.end != hn4 {
+ t.Errorf("Node not appended to 'one' list correctly.")
+ }
+
+ // Dispatch should result in 4 additions.
+ if atomic.LoadInt32(callcount) != 0 {
+ t.Errorf("Something incremented call count before we were expecting it.")
+ }
+ hs.dispatch(c, &Line{Cmd: "One"})
+ <-time.After(time.Millisecond)
+ if atomic.LoadInt32(callcount) != 4 {
+ t.Errorf("Our handler wasn't called four times :-(")
+ }
+
+ // Remove node 3.
+ hn3.Remove()
+ if len(hs.set) != 1 {
+ t.Errorf("Set list count changed after remove().")
+ }
+ if hn3.set != nil || hn3.prev != nil || hn3.next != nil {
+ t.Errorf("Third node for 'one' not removed correctly.")
+ }
+ if hn1.prev != nil || hn1.next != hn2 ||
+ hn2.prev != hn1 || hn2.next != hn4 ||
+ hn4.prev != hn2 || hn4.next != nil {
+ t.Errorf("Third node for 'one' not unlinked correctly.")
+ }
+ if hl.start != hn1 || hl.end != hn4 {
+ t.Errorf("Third node for 'one' changed list pointers.")
+ }
+
+ // Dispatch should result in 3 additions.
+ hs.dispatch(c, &Line{Cmd: "One"})
+ <-time.After(time.Millisecond)
+ if atomic.LoadInt32(callcount) != 7 {
+ t.Errorf("Our handler wasn't called three times :-(")
+ }
+
+ // Remove node 1.
+ hs.remove(hn1)
+ if len(hs.set) != 1 {
+ t.Errorf("Set list count changed after remove().")
+ }
+ if hn1.set != nil || hn1.prev != nil || hn1.next != nil {
+ t.Errorf("First node for 'one' not removed correctly.")
+ }
+ if hn2.prev != nil || hn2.next != hn4 || hn4.prev != hn2 || hn4.next != nil {
+ t.Errorf("First node for 'one' not unlinked correctly.")
+ }
+ if hl.start != hn2 || hl.end != hn4 {
+ t.Errorf("First node for 'one' didn't change list pointers.")
+ }
+
+ // Dispatch should result in 2 additions.
+ hs.dispatch(c, &Line{Cmd: "One"})
+ <-time.After(time.Millisecond)
+ if atomic.LoadInt32(callcount) != 9 {
+ t.Errorf("Our handler wasn't called two times :-(")
+ }
+
+ // Remove node 4.
+ hn4.Remove()
+ if len(hs.set) != 1 {
+ t.Errorf("Set list count changed after remove().")
+ }
+ if hn4.set != nil || hn4.prev != nil || hn4.next != nil {
+ t.Errorf("Fourth node for 'one' not removed correctly.")
+ }
+ if hn2.prev != nil || hn2.next != nil {
+ t.Errorf("Fourth node for 'one' not unlinked correctly.")
+ }
+ if hl.start != hn2 || hl.end != hn2 {
+ t.Errorf("Fourth node for 'one' didn't change list pointers.")
+ }
+
+ // Dispatch should result in 1 addition.
+ hs.dispatch(c, &Line{Cmd: "One"})
+ <-time.After(time.Millisecond)
+ if atomic.LoadInt32(callcount) != 10 {
+ t.Errorf("Our handler wasn't called once :-(")
+ }
+
+ // Remove node 2.
+ hs.remove(hn2)
+ if len(hs.set) != 0 {
+ t.Errorf("Removing last node in 'one' didn't remove list.")
+ }
+ if hn2.set != nil || hn2.prev != nil || hn2.next != nil {
+ t.Errorf("Second node for 'one' not removed correctly.")
+ }
+ if hl.start != nil || hl.end != nil {
+ t.Errorf("Second node for 'one' didn't change list pointers.")
+ }
+
+ // Dispatch should result in NO additions.
+ hs.dispatch(c, &Line{Cmd: "One"})
+ <-time.After(time.Millisecond)
+ if atomic.LoadInt32(callcount) != 10 {
+ t.Errorf("Our handler was called?")
+ }
+}
+
+func TestPanicRecovery(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ recovered := callCheck(t)
+ c.cfg.Recover = func(conn *Conn, line *Line) {
+ if err, ok := recover().(string); ok && err == "panic!" {
+ recovered.call()
+ }
+ }
+ c.HandleFunc(PRIVMSG, func(conn *Conn, line *Line) {
+ panic("panic!")
+ })
+ c.in <- ParseLine(":nick!user@host.com PRIVMSG #channel :OH NO PIGEONS")
+ recovered.assertWasCalled("Failed to recover panic!")
+}
diff --git a/vendor/github.com/fluffle/goirc/client/doc.go b/vendor/github.com/fluffle/goirc/client/doc.go
new file mode 100644
index 0000000..08a79dd
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/doc.go
@@ -0,0 +1,34 @@
+// Package client implements an IRC client. It handles protocol basics
+// such as initial connection and responding to server PINGs, and has
+// optional state tracking support which will keep tabs on every nick
+// present in the same channels as the client. Other features include
+// SSL support, automatic splitting of long lines, and panic recovery
+// for handlers.
+//
+// Incoming IRC messages are parsed into client.Line structs and trigger
+// events based on the IRC verb (e.g. PRIVMSG) of the message. Handlers
+// for these events conform to the client.Handler interface; a HandlerFunc
+// type to wrap bare functions is provided a-la the net/http package.
+//
+// Creating a client, adding a handler and connecting to a server looks
+// soemthing like this, for the simple case:
+//
+// // Create a new client, which will connect with the nick "myNick"
+// irc := client.SimpleClient("myNick")
+//
+// // Add a handler that waits for the "disconnected" event and
+// // closes a channel to signal everything is done.
+// disconnected := make(chan struct{})
+// c.HandleFunc("disconnected", func(c *client.Conn, l *client.Line) {
+// close(disconnected)
+// })
+//
+// // Connect to an IRC server.
+// if err := c.ConnectTo("irc.freenode.net"); err != nil {
+// log.Fatalf("Connection error: %v\n", err)
+// }
+//
+// // Wait for disconnection.
+// <-disconnected
+//
+package client
diff --git a/vendor/github.com/fluffle/goirc/client/handlers.go b/vendor/github.com/fluffle/goirc/client/handlers.go
new file mode 100644
index 0000000..b538579
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/handlers.go
@@ -0,0 +1,105 @@
+package client
+
+// this file contains the basic set of event handlers
+// to manage tracking an irc connection etc.
+
+import (
+ "strings"
+ "time"
+)
+
+// sets up the internal event handlers to do essential IRC protocol things
+var intHandlers = map[string]HandlerFunc{
+ REGISTER: (*Conn).h_REGISTER,
+ "001": (*Conn).h_001,
+ "433": (*Conn).h_433,
+ CTCP: (*Conn).h_CTCP,
+ NICK: (*Conn).h_NICK,
+ PING: (*Conn).h_PING,
+}
+
+func (conn *Conn) addIntHandlers() {
+ for n, h := range intHandlers {
+ // internal handlers are essential for the IRC client
+ // to function, so we don't save their Removers here
+ conn.handle(n, h)
+ }
+}
+
+// Basic ping/pong handler
+func (conn *Conn) h_PING(line *Line) {
+ conn.Pong(line.Args[0])
+}
+
+// Handler for initial registration with server once tcp connection is made.
+func (conn *Conn) h_REGISTER(line *Line) {
+ if conn.cfg.Pass != "" {
+ conn.Pass(conn.cfg.Pass)
+ }
+ conn.Nick(conn.cfg.Me.Nick)
+ conn.User(conn.cfg.Me.Ident, conn.cfg.Me.Name)
+}
+
+// Handler to trigger a CONNECTED event on receipt of numeric 001
+func (conn *Conn) h_001(line *Line) {
+ // we're connected!
+ conn.dispatch(&Line{Cmd: CONNECTED, Time: time.Now()})
+ // and we're being given our hostname (from the server's perspective)
+ t := line.Args[len(line.Args)-1]
+ if idx := strings.LastIndex(t, " "); idx != -1 {
+ t = t[idx+1:]
+ if idx = strings.Index(t, "@"); idx != -1 {
+ if conn.st != nil {
+ me := conn.Me()
+ conn.st.NickInfo(me.Nick, me.Ident, t[idx+1:], me.Name)
+ } else {
+ conn.cfg.Me.Host = t[idx+1:]
+ }
+ }
+ }
+}
+
+// XXX: do we need 005 protocol support message parsing here?
+// probably in the future, but I can't quite be arsed yet.
+/*
+ :irc.pl0rt.org 005 GoTest CMDS=KNOCK,MAP,DCCALLOW,USERIP UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=20 CHANLIMIT=#:20 MAXLIST=b:60,e:60,I:60 NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 :are supported by this server
+ :irc.pl0rt.org 005 GoTest MAXTARGETS=20 WALLCHOPS WATCH=128 WATCHOPTS=A SILENCE=15 MODES=12 CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beI,kfL,lj,psmntirRcOAQKVCuzNSMT NETWORK=bb101.net CASEMAPPING=ascii EXTBAN=~,cqnr ELIST=MNUCT :are supported by this server
+ :irc.pl0rt.org 005 GoTest STATUSMSG=~&@%+ EXCEPTS INVEX :are supported by this server
+*/
+
+// Handler to deal with "433 :Nickname already in use"
+func (conn *Conn) h_433(line *Line) {
+ // Args[1] is the new nick we were attempting to acquire
+ me := conn.Me()
+ neu := conn.cfg.NewNick(line.Args[1])
+ conn.Nick(neu)
+ if !line.argslen(1) {
+ return
+ }
+ // if this is happening before we're properly connected (i.e. the nick
+ // we sent in the initial NICK command is in use) we will not receive
+ // a NICK message to confirm our change of nick, so ReNick here...
+ if line.Args[1] == me.Nick {
+ if conn.st != nil {
+ conn.cfg.Me = conn.st.ReNick(me.Nick, neu)
+ } else {
+ conn.cfg.Me.Nick = neu
+ }
+ }
+}
+
+// Handle VERSION requests and CTCP PING
+func (conn *Conn) h_CTCP(line *Line) {
+ if line.Args[0] == VERSION {
+ conn.CtcpReply(line.Nick, VERSION, conn.cfg.Version)
+ } else if line.Args[0] == PING && line.argslen(2) {
+ conn.CtcpReply(line.Nick, PING, line.Args[2])
+ }
+}
+
+// Handle updating our own NICK if we're not using the state tracker
+func (conn *Conn) h_NICK(line *Line) {
+ if conn.st == nil && line.Nick == conn.cfg.Me.Nick {
+ conn.cfg.Me.Nick = line.Args[0]
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/handlers_test.go b/vendor/github.com/fluffle/goirc/client/handlers_test.go
new file mode 100644
index 0000000..7808022
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/handlers_test.go
@@ -0,0 +1,451 @@
+package client
+
+import (
+ "github.com/fluffle/goirc/state"
+ "github.com/golang/mock/gomock"
+ "testing"
+ "time"
+)
+
+// This test performs a simple end-to-end verification of correct line parsing
+// and event dispatch as well as testing the PING handler. All the other tests
+// in this file will call their respective handlers synchronously, otherwise
+// testing becomes more difficult.
+func TestPING(t *testing.T) {
+ _, s := setUp(t)
+ defer s.tearDown()
+ s.nc.Send("PING :1234567890")
+ s.nc.Expect("PONG :1234567890")
+}
+
+// Test the REGISTER handler matches section 3.1 of rfc2812
+func TestREGISTER(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ c.h_REGISTER(&Line{Cmd: REGISTER})
+ s.nc.Expect("NICK test")
+ s.nc.Expect("USER test 12 * :Testing IRC")
+ s.nc.ExpectNothing()
+
+ c.cfg.Pass = "12345"
+ c.cfg.Me.Ident = "idiot"
+ c.cfg.Me.Name = "I've got the same combination on my luggage!"
+ c.h_REGISTER(&Line{Cmd: REGISTER})
+ s.nc.Expect("PASS 12345")
+ s.nc.Expect("NICK test")
+ s.nc.Expect("USER idiot 12 * :I've got the same combination on my luggage!")
+ s.nc.ExpectNothing()
+}
+
+// Test the handler for 001 / RPL_WELCOME
+func Test001(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ l := ParseLine(":irc.server.org 001 test :Welcome to IRC test!ident@somehost.com")
+ // Set up a handler to detect whether connected handler is called from 001
+ hcon := false
+ c.HandleFunc("connected", func(conn *Conn, line *Line) {
+ hcon = true
+ })
+
+ // Test state tracking first.
+ gomock.InOrder(
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NickInfo("test", "test", "somehost.com", "Testing IRC"),
+ )
+ // Call handler with a valid 001 line
+ c.h_001(l)
+ <-time.After(time.Millisecond)
+ if !hcon {
+ t.Errorf("001 handler did not dispatch connected event.")
+ }
+
+ // Now without state tracking.
+ c.st = nil
+ c.h_001(l)
+ // Check host parsed correctly
+ if c.cfg.Me.Host != "somehost.com" {
+ t.Errorf("Host parsing failed, host is '%s'.", c.cfg.Me.Host)
+ }
+ c.st = s.st
+}
+
+// Test the handler for 433 / ERR_NICKNAMEINUSE
+func Test433(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Call handler with a 433 line, not triggering c.cfg.Me.Renick()
+ s.st.EXPECT().Me().Return(c.cfg.Me)
+ c.h_433(ParseLine(":irc.server.org 433 test new :Nickname is already in use."))
+ s.nc.Expect("NICK new_")
+
+ // Send a line that will trigger a renick. This happens when our wanted
+ // nick is unavailable during initial negotiation, so we must choose a
+ // different one before the connection can proceed. No NICK line will be
+ // sent by the server to confirm nick change in this case.
+ gomock.InOrder(
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().ReNick("test", "test_").Return(c.cfg.Me),
+ )
+ c.h_433(ParseLine(":irc.server.org 433 test test :Nickname is already in use."))
+ s.nc.Expect("NICK test_")
+
+ // Test the code path that *doesn't* involve state tracking.
+ c.st = nil
+ c.h_433(ParseLine(":irc.server.org 433 test test :Nickname is already in use."))
+ s.nc.Expect("NICK test_")
+
+ if c.cfg.Me.Nick != "test_" {
+ t.Errorf("My nick not updated from '%s'.", c.cfg.Me.Nick)
+ }
+ c.st = s.st
+}
+
+// Test the handler for NICK messages when state tracking is disabled
+func TestNICK(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // State tracking is enabled by default in setUp
+ c.st = nil
+
+ // Call handler with a NICK line changing "our" nick to test1.
+ c.h_NICK(ParseLine(":test!test@somehost.com NICK :test1"))
+
+ // Verify that our Nick has changed
+ if c.cfg.Me.Nick != "test1" {
+ t.Errorf("NICK did not result in changing our nick.")
+ }
+
+ // Send a NICK line for something that isn't us.
+ c.h_NICK(ParseLine(":blah!moo@cows.com NICK :milk"))
+
+ // Verify that our Nick hasn't changed
+ if c.cfg.Me.Nick != "test1" {
+ t.Errorf("NICK did not result in changing our nick.")
+ }
+
+ // Re-enable state tracking and send a line that *should* change nick.
+ c.st = s.st
+ c.h_NICK(ParseLine(":test1!test@somehost.com NICK :test2"))
+
+ // Verify that our Nick hasn't changed (should be handled by h_STNICK).
+ if c.cfg.Me.Nick != "test1" {
+ t.Errorf("NICK changed our nick when state tracking enabled.")
+ }
+}
+
+// Test the handler for CTCP messages
+func TestCTCP(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Call handler with CTCP VERSION
+ c.h_CTCP(ParseLine(":blah!moo@cows.com PRIVMSG test :\001VERSION\001"))
+
+ // Expect a version reply
+ s.nc.Expect("NOTICE blah :\001VERSION Powered by GoIRC\001")
+
+ // Call handler with CTCP PING
+ c.h_CTCP(ParseLine(":blah!moo@cows.com PRIVMSG test :\001PING 1234567890\001"))
+
+ // Expect a ping reply
+ s.nc.Expect("NOTICE blah :\001PING 1234567890\001")
+
+ // Call handler with CTCP UNKNOWN
+ c.h_CTCP(ParseLine(":blah!moo@cows.com PRIVMSG test :\001UNKNOWN ctcp\001"))
+}
+
+// Test the handler for JOIN messages
+func TestJOIN(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // The state tracker should be creating a new channel in this first test
+ chan1 := &state.Channel{Name: "#test1"}
+
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(nil),
+ s.st.EXPECT().GetNick("test").Return(c.cfg.Me),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NewChannel("#test1").Return(chan1),
+ s.st.EXPECT().Associate("#test1", "test"),
+ )
+
+ // Use #test1 to test expected behaviour
+ // Call handler with JOIN by test to #test1
+ c.h_JOIN(ParseLine(":test!test@somehost.com JOIN :#test1"))
+
+ // Verify that the MODE and WHO commands are sent correctly
+ s.nc.Expect("MODE #test1")
+ s.nc.Expect("WHO #test1")
+
+ // In this second test, we should be creating a new nick
+ nick1 := &state.Nick{Nick: "user1"}
+
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(chan1),
+ s.st.EXPECT().GetNick("user1").Return(nil),
+ s.st.EXPECT().NewNick("user1").Return(nick1),
+ s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "").Return(nick1),
+ s.st.EXPECT().Associate("#test1", "user1"),
+ )
+
+ // OK, now #test1 exists, JOIN another user we don't know about
+ c.h_JOIN(ParseLine(":user1!ident1@host1.com JOIN :#test1"))
+
+ // Verify that the WHO command is sent correctly
+ s.nc.Expect("WHO user1")
+
+ // In this third test, we'll be pretending we know about the nick already.
+ nick2 := &state.Nick{Nick: "user2"}
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(chan1),
+ s.st.EXPECT().GetNick("user2").Return(nick2),
+ s.st.EXPECT().Associate("#test1", "user2"),
+ )
+ c.h_JOIN(ParseLine(":user2!ident2@host2.com JOIN :#test1"))
+
+ // Test error paths
+ gomock.InOrder(
+ // unknown channel, unknown nick
+ s.st.EXPECT().GetChannel("#test2").Return(nil),
+ s.st.EXPECT().GetNick("blah").Return(nil),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ // unknown channel, known nick that isn't Me.
+ s.st.EXPECT().GetChannel("#test2").Return(nil),
+ s.st.EXPECT().GetNick("user2").Return(nick2),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ )
+ c.h_JOIN(ParseLine(":blah!moo@cows.com JOIN :#test2"))
+ c.h_JOIN(ParseLine(":user2!ident2@host2.com JOIN :#test2"))
+}
+
+// Test the handler for PART messages
+func TestPART(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // PART should dissociate a nick from a channel.
+ s.st.EXPECT().Dissociate("#test1", "user1")
+ c.h_PART(ParseLine(":user1!ident1@host1.com PART #test1 :Bye!"))
+}
+
+// Test the handler for KICK messages
+// (this is very similar to the PART message test)
+func TestKICK(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // KICK should dissociate a nick from a channel.
+ s.st.EXPECT().Dissociate("#test1", "user1")
+ c.h_KICK(ParseLine(":test!test@somehost.com KICK #test1 user1 :Bye!"))
+}
+
+// Test the handler for QUIT messages
+func TestQUIT(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Have user1 QUIT. All possible errors handled by state tracker \o/
+ s.st.EXPECT().DelNick("user1")
+ c.h_QUIT(ParseLine(":user1!ident1@host1.com QUIT :Bye!"))
+}
+
+// Test the handler for MODE messages
+func TestMODE(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Channel modes
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}),
+ s.st.EXPECT().ChannelModes("#test1", "+sk", "somekey"),
+ )
+ c.h_MODE(ParseLine(":user1!ident1@host1.com MODE #test1 +sk somekey"))
+
+ // Nick modes for Me.
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("test").Return(nil),
+ s.st.EXPECT().GetNick("test").Return(c.cfg.Me),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NickModes("test", "+i"),
+ )
+ c.h_MODE(ParseLine(":test!test@somehost.com MODE test +i"))
+
+ // Check error paths
+ gomock.InOrder(
+ // send a valid user mode that's not us
+ s.st.EXPECT().GetChannel("user1").Return(nil),
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ // Send a random mode for an unknown channel
+ s.st.EXPECT().GetChannel("#test2").Return(nil),
+ s.st.EXPECT().GetNick("#test2").Return(nil),
+ )
+ c.h_MODE(ParseLine(":user1!ident1@host1.com MODE user1 +w"))
+ c.h_MODE(ParseLine(":user1!ident1@host1.com MODE #test2 +is"))
+}
+
+// Test the handler for TOPIC messages
+func TestTOPIC(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure TOPIC reply calls Topic
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}),
+ s.st.EXPECT().Topic("#test1", "something something"),
+ )
+ c.h_TOPIC(ParseLine(":user1!ident1@host1.com TOPIC #test1 :something something"))
+
+ // Check error paths -- send a topic for an unknown channel
+ s.st.EXPECT().GetChannel("#test2").Return(nil)
+ c.h_TOPIC(ParseLine(":user1!ident1@host1.com TOPIC #test2 :dark side"))
+}
+
+// Test the handler for 311 / RPL_WHOISUSER
+func Test311(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure 311 reply calls NickInfo
+ gomock.InOrder(
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "name"),
+ )
+ c.h_311(ParseLine(":irc.server.org 311 test user1 ident1 host1.com * :name"))
+
+ // Check error paths -- send a 311 for an unknown nick
+ s.st.EXPECT().GetNick("user2").Return(nil)
+ c.h_311(ParseLine(":irc.server.org 311 test user2 ident2 host2.com * :dongs"))
+}
+
+// Test the handler for 324 / RPL_CHANNELMODEIS
+func Test324(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure 324 reply calls ChannelModes
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}),
+ s.st.EXPECT().ChannelModes("#test1", "+sk", "somekey"),
+ )
+ c.h_324(ParseLine(":irc.server.org 324 test #test1 +sk somekey"))
+
+ // Check error paths -- send 324 for an unknown channel
+ s.st.EXPECT().GetChannel("#test2").Return(nil)
+ c.h_324(ParseLine(":irc.server.org 324 test #test2 +pmt"))
+}
+
+// Test the handler for 332 / RPL_TOPIC
+func Test332(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure 332 reply calls Topic
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}),
+ s.st.EXPECT().Topic("#test1", "something something"),
+ )
+ c.h_332(ParseLine(":irc.server.org 332 test #test1 :something something"))
+
+ // Check error paths -- send 332 for an unknown channel
+ s.st.EXPECT().GetChannel("#test2").Return(nil)
+ c.h_332(ParseLine(":irc.server.org 332 test #test2 :dark side"))
+}
+
+// Test the handler for 352 / RPL_WHOREPLY
+func Test352(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure 352 reply calls NickInfo and NickModes
+ gomock.InOrder(
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "name"),
+ )
+ c.h_352(ParseLine(":irc.server.org 352 test #test1 ident1 host1.com irc.server.org user1 G :0 name"))
+
+ // Check that modes are set correctly from WHOREPLY
+ gomock.InOrder(
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "name"),
+ s.st.EXPECT().NickModes("user1", "+o"),
+ s.st.EXPECT().NickModes("user1", "+i"),
+ )
+ c.h_352(ParseLine(":irc.server.org 352 test #test1 ident1 host1.com irc.server.org user1 H* :0 name"))
+
+ // Check error paths -- send a 352 for an unknown nick
+ s.st.EXPECT().GetNick("user2").Return(nil)
+ c.h_352(ParseLine(":irc.server.org 352 test #test2 ident2 host2.com irc.server.org user2 G :0 fooo"))
+}
+
+// Test the handler for 353 / RPL_NAMREPLY
+func Test353(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // 353 handler is called twice, so GetChannel will be called twice
+ s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}).Times(2)
+ gomock.InOrder(
+ // "test" is Me, i am known, and already on the channel
+ s.st.EXPECT().GetNick("test").Return(c.cfg.Me),
+ s.st.EXPECT().IsOn("#test1", "test").Return(&state.ChanPrivs{}, true),
+ // user1 is known, but not on the channel, so should be associated
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().IsOn("#test1", "user1").Return(nil, false),
+ s.st.EXPECT().Associate("#test1", "user1").Return(&state.ChanPrivs{}),
+ s.st.EXPECT().ChannelModes("#test1", "+o", "user1"),
+ )
+ for n, m := range map[string]string{
+ "user2": "",
+ "voice": "+v",
+ "halfop": "+h",
+ "op": "+o",
+ "admin": "+a",
+ "owner": "+q",
+ } {
+ calls := []*gomock.Call{
+ s.st.EXPECT().GetNick(n).Return(nil),
+ s.st.EXPECT().NewNick(n).Return(&state.Nick{Nick: n}),
+ s.st.EXPECT().IsOn("#test1", n).Return(nil, false),
+ s.st.EXPECT().Associate("#test1", n).Return(&state.ChanPrivs{}),
+ }
+ if m != "" {
+ calls = append(calls, s.st.EXPECT().ChannelModes("#test1", m, n))
+ }
+ gomock.InOrder(calls...)
+ }
+
+ // Send a couple of names replies (complete with trailing space)
+ c.h_353(ParseLine(":irc.server.org 353 test = #test1 :test @user1 user2 +voice "))
+ c.h_353(ParseLine(":irc.server.org 353 test = #test1 :%halfop @op &admin ~owner "))
+
+ // Check error paths -- send 353 for an unknown channel
+ s.st.EXPECT().GetChannel("#test2").Return(nil)
+ c.h_353(ParseLine(":irc.server.org 353 test = #test2 :test ~user3"))
+}
+
+// Test the handler for 671 (unreal specific)
+func Test671(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure 671 reply calls NickModes
+ gomock.InOrder(
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().NickModes("user1", "+z"),
+ )
+ c.h_671(ParseLine(":irc.server.org 671 test user1 :some ignored text"))
+
+ // Check error paths -- send a 671 for an unknown nick
+ s.st.EXPECT().GetNick("user2").Return(nil)
+ c.h_671(ParseLine(":irc.server.org 671 test user2 :some ignored text"))
+}
diff --git a/vendor/github.com/fluffle/goirc/client/line.go b/vendor/github.com/fluffle/goirc/client/line.go
new file mode 100644
index 0000000..bfa473a
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/line.go
@@ -0,0 +1,216 @@
+package client
+
+import (
+ "runtime"
+ "strings"
+ "time"
+
+ "github.com/fluffle/goirc/logging"
+)
+
+var tagsReplacer = strings.NewReplacer("\\:", ";", "\\s", " ", "\\r", "\r", "\\n", "\n")
+
+// We parse an incoming line into this struct. Line.Cmd is used as the trigger
+// name for incoming event handlers and is the IRC verb, the first sequence
+// of non-whitespace characters after ":nick!user@host", e.g. PRIVMSG.
+// Raw =~ ":nick!user@host cmd args[] :text"
+// Src == "nick!user@host"
+// Cmd == e.g. PRIVMSG, 332
+type Line struct {
+ Tags map[string]string
+ Nick, Ident, Host, Src string
+ Cmd, Raw string
+ Args []string
+ Time time.Time
+}
+
+// Copy returns a deep copy of the Line.
+func (l *Line) Copy() *Line {
+ nl := *l
+ nl.Args = make([]string, len(l.Args))
+ copy(nl.Args, l.Args)
+ if l.Tags != nil {
+ nl.Tags = make(map[string]string)
+ for k, v := range l.Tags {
+ nl.Tags[k] = v
+ }
+ }
+ return &nl
+}
+
+// Text returns the contents of the text portion of a line. This only really
+// makes sense for lines with a :text part, but there are a lot of them.
+func (line *Line) Text() string {
+ if len(line.Args) > 0 {
+ return line.Args[len(line.Args)-1]
+ }
+ return ""
+}
+
+// Target returns the contextual target of the line, usually the first Arg
+// for the IRC verb. If the line was broadcast from a channel, the target
+// will be that channel. If the line was sent directly by a user, the target
+// will be that user.
+func (line *Line) Target() string {
+ // TODO(fluffle): Add 005 CHANTYPES parsing for this?
+ switch line.Cmd {
+ case PRIVMSG, NOTICE, ACTION:
+ if !line.Public() {
+ return line.Nick
+ }
+ case CTCP, CTCPREPLY:
+ if !line.Public() {
+ return line.Nick
+ }
+ return line.Args[1]
+ }
+ if len(line.Args) > 0 {
+ return line.Args[0]
+ }
+ return ""
+}
+
+// Public returns true if the line is the result of an IRC user sending
+// a message to a channel the client has joined instead of directly
+// to the client.
+//
+// NOTE: This is very permissive, allowing all 4 RFC channel types even if
+// your server doesn't technically support them.
+func (line *Line) Public() bool {
+ switch line.Cmd {
+ case PRIVMSG, NOTICE, ACTION:
+ switch line.Args[0][0] {
+ case '#', '&', '+', '!':
+ return true
+ }
+ case CTCP, CTCPREPLY:
+ // CTCP prepends the CTCP verb to line.Args, thus for the message
+ // :nick!user@host PRIVMSG #foo :\001BAR baz\001
+ // line.Args contains: []string{"BAR", "#foo", "baz"}
+ // TODO(fluffle): Arguably this is broken, and we should have
+ // line.Args containing: []string{"#foo", "BAR", "baz"}
+ // ... OR change conn.Ctcp()'s argument order to be consistent.
+ switch line.Args[1][0] {
+ case '#', '&', '+', '!':
+ return true
+ }
+ }
+ return false
+}
+
+// ParseLine creates a Line from an incoming message from the IRC server.
+//
+// It contains special casing for CTCP messages, most notably CTCP ACTION.
+// All CTCP messages have the \001 bytes stripped from the message and the
+// CTCP command separated from any subsequent text. Then, CTCP ACTIONs are
+// rewritten such that Line.Cmd == ACTION. Other CTCP messages have Cmd
+// set to CTCP or CTCPREPLY, and the CTCP command prepended to line.Args.
+//
+// ParseLine also parses IRCv3 tags, if received. If a line does not have
+// the tags section, Line.Tags will be nil. Tags are optional, and will
+// only be included after the correct CAP command.
+//
+// http://ircv3.net/specs/core/capability-negotiation-3.1.html
+// http://ircv3.net/specs/core/message-tags-3.2.html
+func ParseLine(s string) *Line {
+ line := &Line{Raw: s}
+
+ if s == "" {
+ return nil
+ }
+
+ if s[0] == '@' {
+ var rawTags string
+ line.Tags = make(map[string]string)
+ if idx := strings.Index(s, " "); idx != -1 {
+ rawTags, s = s[1:idx], s[idx+1:]
+ } else {
+ return nil
+ }
+
+ // ; is represented as \: in a tag, so it's safe to split on ;
+ for _, tag := range strings.Split(rawTags, ";") {
+ if tag == "" {
+ continue
+ }
+
+ pair := strings.SplitN(tagsReplacer.Replace(tag), "=", 2)
+ if len(pair) < 2 {
+ line.Tags[tag] = ""
+ } else {
+ line.Tags[pair[0]] = pair[1]
+ }
+ }
+ }
+
+ if s[0] == ':' {
+ // remove a source and parse it
+ if idx := strings.Index(s, " "); idx != -1 {
+ line.Src, s = s[1:idx], s[idx+1:]
+ } else {
+ // pretty sure we shouldn't get here ...
+ return nil
+ }
+
+ // src can be the hostname of the irc server or a nick!user@host
+ line.Host = line.Src
+ nidx, uidx := strings.Index(line.Src, "!"), strings.Index(line.Src, "@")
+ if uidx != -1 && nidx != -1 {
+ line.Nick = line.Src[:nidx]
+ line.Ident = line.Src[nidx+1 : uidx]
+ line.Host = line.Src[uidx+1:]
+ }
+ }
+
+ // now we're here, we've parsed a :nick!user@host or :server off
+ // s should contain "cmd args[] :text"
+ args := strings.SplitN(s, " :", 2)
+ if len(args) > 1 {
+ args = append(strings.Fields(args[0]), args[1])
+ } else {
+ args = strings.Fields(args[0])
+ }
+ line.Cmd = strings.ToUpper(args[0])
+ if len(args) > 1 {
+ line.Args = args[1:]
+ }
+
+ // So, I think CTCP and (in particular) CTCP ACTION are better handled as
+ // separate events as opposed to forcing people to have gargantuan
+ // handlers to cope with the possibilities.
+ if (line.Cmd == PRIVMSG || line.Cmd == NOTICE) &&
+ len(line.Args[1]) > 2 &&
+ strings.HasPrefix(line.Args[1], "\001") &&
+ strings.HasSuffix(line.Args[1], "\001") {
+ // WOO, it's a CTCP message
+ t := strings.SplitN(strings.Trim(line.Args[1], "\001"), " ", 2)
+ if len(t) > 1 {
+ // Replace the line with the unwrapped CTCP
+ line.Args[1] = t[1]
+ }
+ if c := strings.ToUpper(t[0]); c == ACTION && line.Cmd == PRIVMSG {
+ // make a CTCP ACTION it's own event a-la PRIVMSG
+ line.Cmd = c
+ } else {
+ // otherwise, dispatch a generic CTCP/CTCPREPLY event that
+ // contains the type of CTCP in line.Args[0]
+ if line.Cmd == PRIVMSG {
+ line.Cmd = CTCP
+ } else {
+ line.Cmd = CTCPREPLY
+ }
+ line.Args = append([]string{c}, line.Args...)
+ }
+ }
+ return line
+}
+
+func (line *Line) argslen(minlen int) bool {
+ pc, _, _, _ := runtime.Caller(1)
+ fn := runtime.FuncForPC(pc)
+ if len(line.Args) <= minlen {
+ logging.Warn("%s: too few arguments: %s", fn.Name(), strings.Join(line.Args, " "))
+ return false
+ }
+ return true
+}
diff --git a/vendor/github.com/fluffle/goirc/client/line_test.go b/vendor/github.com/fluffle/goirc/client/line_test.go
new file mode 100644
index 0000000..88b758d
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/line_test.go
@@ -0,0 +1,186 @@
+package client
+
+import (
+ "reflect"
+ "testing"
+ "time"
+)
+
+func TestLineCopy(t *testing.T) {
+ l1 := &Line{
+ Tags: map[string]string{"foo": "bar", "fizz": "buzz"},
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host",
+ Src: "src",
+ Cmd: "cmd",
+ Raw: "raw",
+ Args: []string{"arg", "text"},
+ Time: time.Now(),
+ }
+
+ l2 := l1.Copy()
+
+ // Ugly. Couldn't be bothered to bust out reflect and actually think.
+ if l2.Tags == nil || l2.Tags["foo"] != "bar" || l2.Tags["fizz"] != "buzz" ||
+ l2.Nick != "nick" || l2.Ident != "ident" || l2.Host != "host" ||
+ l2.Src != "src" || l2.Cmd != "cmd" || l2.Raw != "raw" ||
+ l2.Args[0] != "arg" || l2.Args[1] != "text" || l2.Time != l1.Time {
+ t.Errorf("Line not copied correctly")
+ t.Errorf("l1: %#v\nl2: %#v", l1, l2)
+ }
+
+ // Now, modify l2 and verify l1 not changed
+ l2.Tags["foo"] = "baz"
+ l2.Nick = l2.Nick[1:]
+ l2.Ident = "foo"
+ l2.Host = ""
+ l2.Args[0] = l2.Args[0][1:]
+ l2.Args[1] = "bar"
+ l2.Time = time.Now()
+
+ if l2.Tags == nil || l2.Tags["foo"] != "baz" || l2.Tags["fizz"] != "buzz" ||
+ l1.Nick != "nick" || l1.Ident != "ident" || l1.Host != "host" ||
+ l1.Src != "src" || l1.Cmd != "cmd" || l1.Raw != "raw" ||
+ l1.Args[0] != "arg" || l1.Args[1] != "text" || l1.Time == l2.Time {
+ t.Errorf("Original modified when copy changed")
+ t.Errorf("l1: %#v\nl2: %#v", l1, l2)
+ }
+}
+
+func TestLineText(t *testing.T) {
+ tests := []struct {
+ in *Line
+ out string
+ }{
+ {&Line{}, ""},
+ {&Line{Args: []string{"one thing"}}, "one thing"},
+ {&Line{Args: []string{"one", "two"}}, "two"},
+ }
+
+ for i, test := range tests {
+ out := test.in.Text()
+ if out != test.out {
+ t.Errorf("test %d: expected: '%s', got '%s'", i, test.out, out)
+ }
+ }
+}
+
+func TestLineTarget(t *testing.T) {
+ tests := []struct {
+ in *Line
+ out string
+ }{
+ {&Line{}, ""},
+ {&Line{Cmd: JOIN, Args: []string{"#foo"}}, "#foo"},
+ {&Line{Cmd: PART, Args: []string{"#foo", "bye"}}, "#foo"},
+ {&Line{Cmd: PRIVMSG, Args: []string{"Me", "la"}, Nick: "Them"}, "Them"},
+ {&Line{Cmd: NOTICE, Args: []string{"Me", "la"}, Nick: "Them"}, "Them"},
+ {&Line{Cmd: ACTION, Args: []string{"Me", "la"}, Nick: "Them"}, "Them"},
+ {&Line{Cmd: CTCP, Args: []string{"PING", "Me", "1"}, Nick: "Them"}, "Them"},
+ {&Line{Cmd: CTCPREPLY, Args: []string{"PONG", "Me", "2"}, Nick: "Them"}, "Them"},
+ {&Line{Cmd: PRIVMSG, Args: []string{"#foo", "la"}, Nick: "Them"}, "#foo"},
+ {&Line{Cmd: NOTICE, Args: []string{"&foo", "la"}, Nick: "Them"}, "&foo"},
+ {&Line{Cmd: ACTION, Args: []string{"!foo", "la"}, Nick: "Them"}, "!foo"},
+ {&Line{Cmd: CTCP, Args: []string{"PING", "#foo", "1"}, Nick: "Them"}, "#foo"},
+ {&Line{Cmd: CTCPREPLY, Args: []string{"PONG", "#foo", "2"}, Nick: "Them"}, "#foo"},
+ }
+
+ for i, test := range tests {
+ out := test.in.Target()
+ if out != test.out {
+ t.Errorf("test %d: expected: '%s', got '%s'", i, test.out, out)
+ }
+ }
+}
+
+func TestLineTags(t *testing.T) {
+ tests := []struct {
+ in string
+ out *Line
+ }{
+ { // Make sure ERROR lines work
+ "ERROR :Closing Link: example.org (Too many user connections (global))",
+ &Line{
+ Nick: "",
+ Ident: "",
+ Host: "",
+ Src: "",
+ Cmd: ERROR,
+ Raw: "ERROR :Closing Link: example.org (Too many user connections (global))",
+ Args: []string{"Closing Link: example.org (Too many user connections (global))"},
+ },
+ },
+ { // Make sure non-tagged lines work
+ ":nick!ident@host.com PRIVMSG me :Hello",
+ &Line{
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host.com",
+ Src: "nick!ident@host.com",
+ Cmd: PRIVMSG,
+ Raw: ":nick!ident@host.com PRIVMSG me :Hello",
+ Args: []string{"me", "Hello"},
+ },
+ },
+ { // Tags example from the spec
+ "@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello",
+ &Line{
+ Tags: map[string]string{"aaa": "bbb", "ccc": "", "example.com/ddd": "eee"},
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host.com",
+ Src: "nick!ident@host.com",
+ Cmd: PRIVMSG,
+ Raw: "@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello",
+ Args: []string{"me", "Hello"},
+ },
+ },
+ { // Test escaped characters
+ "@\\:=\\:;\\s=\\s;\\r=\\r;\\n=\\n :nick!ident@host.com PRIVMSG me :Hello",
+ &Line{
+ Tags: map[string]string{";": ";", " ": " ", "\r": "\r", "\n": "\n"},
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host.com",
+ Src: "nick!ident@host.com",
+ Cmd: PRIVMSG,
+ Raw: "@\\:=\\:;\\s=\\s;\\r=\\r;\\n=\\n :nick!ident@host.com PRIVMSG me :Hello",
+ Args: []string{"me", "Hello"},
+ },
+ },
+ { // Skip empty tag
+ "@a=a; :nick!ident@host.com PRIVMSG me :Hello",
+ &Line{
+ Tags: map[string]string{"a": "a"},
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host.com",
+ Src: "nick!ident@host.com",
+ Cmd: PRIVMSG,
+ Raw: "@a=a; :nick!ident@host.com PRIVMSG me :Hello",
+ Args: []string{"me", "Hello"},
+ },
+ },
+ { // = in tag
+ "@a=a=a; :nick!ident@host.com PRIVMSG me :Hello",
+ &Line{
+ Tags: map[string]string{"a": "a=a"},
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host.com",
+ Src: "nick!ident@host.com",
+ Cmd: PRIVMSG,
+ Raw: "@a=a=a; :nick!ident@host.com PRIVMSG me :Hello",
+ Args: []string{"me", "Hello"},
+ },
+ },
+ }
+
+ for i, test := range tests {
+ got := ParseLine(test.in)
+ if !reflect.DeepEqual(got, test.out) {
+ t.Errorf("test %d:\nexpected %#v\ngot %#v", i, test.out, got)
+ }
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/mocknetconn_test.go b/vendor/github.com/fluffle/goirc/client/mocknetconn_test.go
new file mode 100644
index 0000000..e736c88
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/mocknetconn_test.go
@@ -0,0 +1,154 @@
+package client
+
+import (
+ "io"
+ "net"
+ "os"
+ "strings"
+ "testing"
+ "time"
+)
+
+type mockNetConn struct {
+ *testing.T
+
+ In, Out chan string
+ in, out chan []byte
+ die chan struct{}
+
+ closed bool
+ rt, wt time.Time
+}
+
+func MockNetConn(t *testing.T) *mockNetConn {
+ // Our mock connection is a testing object
+ m := &mockNetConn{T: t, die: make(chan struct{})}
+
+ // buffer input
+ m.In = make(chan string, 20)
+ m.in = make(chan []byte)
+ go func() {
+ for {
+ select {
+ case <-m.die:
+ return
+ case s := <-m.In:
+ m.in <- []byte(s)
+ }
+ }
+ }()
+
+ // buffer output
+ m.Out = make(chan string)
+ m.out = make(chan []byte, 20)
+ go func() {
+ for {
+ select {
+ case <-m.die:
+ return
+ case b := <-m.out:
+ m.Out <- string(b)
+ }
+ }
+ }()
+
+ return m
+}
+
+// Test helpers
+func (m *mockNetConn) Send(s string) {
+ m.In <- s + "\r\n"
+}
+
+func (m *mockNetConn) Expect(e string) {
+ select {
+ case <-time.After(time.Millisecond):
+ m.Errorf("Mock connection did not receive expected output.\n\t"+
+ "Expected: '%s', got nothing.", e)
+ case s := <-m.Out:
+ s = strings.Trim(s, "\r\n")
+ if e != s {
+ m.Errorf("Mock connection received unexpected value.\n\t"+
+ "Expected: '%s'\n\tGot: '%s'", e, s)
+ }
+ }
+}
+
+func (m *mockNetConn) ExpectNothing() {
+ select {
+ case <-time.After(time.Millisecond):
+ case s := <-m.Out:
+ s = strings.Trim(s, "\r\n")
+ m.Errorf("Mock connection received unexpected output.\n\t"+
+ "Expected nothing, got: '%s'", s)
+ }
+}
+
+// Implement net.Conn interface
+func (m *mockNetConn) Read(b []byte) (int, error) {
+ if m.Closed() {
+ return 0, os.ErrInvalid
+ }
+ l := 0
+ select {
+ case s := <-m.in:
+ l = len(s)
+ copy(b, s)
+ case <-m.die:
+ return 0, io.EOF
+ }
+ return l, nil
+}
+
+func (m *mockNetConn) Write(s []byte) (int, error) {
+ if m.Closed() {
+ return 0, os.ErrInvalid
+ }
+ b := make([]byte, len(s))
+ copy(b, s)
+ m.out <- b
+ return len(s), nil
+}
+
+func (m *mockNetConn) Close() error {
+ if m.Closed() {
+ return os.ErrInvalid
+ }
+ // Shut down *ALL* the goroutines!
+ // This will trigger an EOF event in Read() too
+ close(m.die)
+ return nil
+}
+
+func (m *mockNetConn) Closed() bool {
+ select {
+ case <-m.die:
+ return true
+ default:
+ return false
+ }
+}
+
+func (m *mockNetConn) LocalAddr() net.Addr {
+ return &net.IPAddr{IP: net.IPv4(127, 0, 0, 1)}
+}
+
+func (m *mockNetConn) RemoteAddr() net.Addr {
+ return &net.IPAddr{IP: net.IPv4(127, 0, 0, 1)}
+}
+
+func (m *mockNetConn) SetDeadline(t time.Time) error {
+ m.rt = t
+ m.wt = t
+ return nil
+}
+
+func (m *mockNetConn) SetReadDeadline(t time.Time) error {
+ m.rt = t
+ return nil
+}
+
+func (m *mockNetConn) SetWriteDeadline(t time.Time) error {
+ m.wt = t
+ return nil
+}
diff --git a/vendor/github.com/fluffle/goirc/client/state_handlers.go b/vendor/github.com/fluffle/goirc/client/state_handlers.go
new file mode 100644
index 0000000..847679c
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/state_handlers.go
@@ -0,0 +1,262 @@
+package client
+
+// this file contains the extra set of event handlers
+// to manage tracking state for an IRC connection
+
+import (
+ "strings"
+
+ "github.com/fluffle/goirc/logging"
+)
+
+var stHandlers = map[string]HandlerFunc{
+ "JOIN": (*Conn).h_JOIN,
+ "KICK": (*Conn).h_KICK,
+ "MODE": (*Conn).h_MODE,
+ "NICK": (*Conn).h_STNICK,
+ "PART": (*Conn).h_PART,
+ "QUIT": (*Conn).h_QUIT,
+ "TOPIC": (*Conn).h_TOPIC,
+ "311": (*Conn).h_311,
+ "324": (*Conn).h_324,
+ "332": (*Conn).h_332,
+ "352": (*Conn).h_352,
+ "353": (*Conn).h_353,
+ "671": (*Conn).h_671,
+}
+
+func (conn *Conn) addSTHandlers() {
+ for n, h := range stHandlers {
+ conn.stRemovers = append(conn.stRemovers, conn.handle(n, h))
+ }
+}
+
+func (conn *Conn) delSTHandlers() {
+ for _, h := range conn.stRemovers {
+ h.Remove()
+ }
+ conn.stRemovers = conn.stRemovers[:0]
+}
+
+// Handle NICK messages that need to update the state tracker
+func (conn *Conn) h_STNICK(line *Line) {
+ // all nicks should be handled the same way, our own included
+ conn.st.ReNick(line.Nick, line.Args[0])
+}
+
+// Handle JOINs to channels to maintain state
+func (conn *Conn) h_JOIN(line *Line) {
+ ch := conn.st.GetChannel(line.Args[0])
+ nk := conn.st.GetNick(line.Nick)
+ if ch == nil {
+ // first we've seen of this channel, so should be us joining it
+ // NOTE this will also take care of nk == nil && ch == nil
+ if !conn.Me().Equals(nk) {
+ logging.Warn("irc.JOIN(): JOIN to unknown channel %s received "+
+ "from (non-me) nick %s", line.Args[0], line.Nick)
+ return
+ }
+ conn.st.NewChannel(line.Args[0])
+ // since we don't know much about this channel, ask server for info
+ // we get the channel users automatically in 353 and the channel
+ // topic in 332 on join, so we just need to get the modes
+ conn.Mode(line.Args[0])
+ // sending a WHO for the channel is MUCH more efficient than
+ // triggering a WHOIS on every nick from the 353 handler
+ conn.Who(line.Args[0])
+ }
+ if nk == nil {
+ // this is the first we've seen of this nick
+ conn.st.NewNick(line.Nick)
+ conn.st.NickInfo(line.Nick, line.Ident, line.Host, "")
+ // since we don't know much about this nick, ask server for info
+ conn.Who(line.Nick)
+ }
+ // this takes care of both nick and channel linking \o/
+ conn.st.Associate(line.Args[0], line.Nick)
+}
+
+// Handle PARTs from channels to maintain state
+func (conn *Conn) h_PART(line *Line) {
+ conn.st.Dissociate(line.Args[0], line.Nick)
+}
+
+// Handle KICKs from channels to maintain state
+func (conn *Conn) h_KICK(line *Line) {
+ if !line.argslen(1) {
+ return
+ }
+ // XXX: this won't handle autorejoining channels on KICK
+ // it's trivial to do this in a seperate handler...
+ conn.st.Dissociate(line.Args[0], line.Args[1])
+}
+
+// Handle other people's QUITs
+func (conn *Conn) h_QUIT(line *Line) {
+ conn.st.DelNick(line.Nick)
+}
+
+// Handle MODE changes for channels we know about (and our nick personally)
+func (conn *Conn) h_MODE(line *Line) {
+ if !line.argslen(1) {
+ return
+ }
+ if ch := conn.st.GetChannel(line.Args[0]); ch != nil {
+ // channel modes first
+ conn.st.ChannelModes(line.Args[0], line.Args[1], line.Args[2:]...)
+ } else if nk := conn.st.GetNick(line.Args[0]); nk != nil {
+ // nick mode change, should be us
+ if !conn.Me().Equals(nk) {
+ logging.Warn("irc.MODE(): recieved MODE %s for (non-me) nick %s",
+ line.Args[1], line.Args[0])
+ return
+ }
+ conn.st.NickModes(line.Args[0], line.Args[1])
+ } else {
+ logging.Warn("irc.MODE(): not sure what to do with MODE %s",
+ strings.Join(line.Args, " "))
+ }
+}
+
+// Handle TOPIC changes for channels
+func (conn *Conn) h_TOPIC(line *Line) {
+ if !line.argslen(1) {
+ return
+ }
+ if ch := conn.st.GetChannel(line.Args[0]); ch != nil {
+ conn.st.Topic(line.Args[0], line.Args[1])
+ } else {
+ logging.Warn("irc.TOPIC(): topic change on unknown channel %s",
+ line.Args[0])
+ }
+}
+
+// Handle 311 whois reply
+func (conn *Conn) h_311(line *Line) {
+ if !line.argslen(5) {
+ return
+ }
+ if nk := conn.st.GetNick(line.Args[1]); (nk != nil) && !conn.Me().Equals(nk) {
+ conn.st.NickInfo(line.Args[1], line.Args[2], line.Args[3], line.Args[5])
+ } else {
+ logging.Warn("irc.311(): received WHOIS info for unknown nick %s",
+ line.Args[1])
+ }
+}
+
+// Handle 324 mode reply
+func (conn *Conn) h_324(line *Line) {
+ if !line.argslen(2) {
+ return
+ }
+ if ch := conn.st.GetChannel(line.Args[1]); ch != nil {
+ conn.st.ChannelModes(line.Args[1], line.Args[2], line.Args[3:]...)
+ } else {
+ logging.Warn("irc.324(): received MODE settings for unknown channel %s",
+ line.Args[1])
+ }
+}
+
+// Handle 332 topic reply on join to channel
+func (conn *Conn) h_332(line *Line) {
+ if !line.argslen(2) {
+ return
+ }
+ if ch := conn.st.GetChannel(line.Args[1]); ch != nil {
+ conn.st.Topic(line.Args[1], line.Args[2])
+ } else {
+ logging.Warn("irc.332(): received TOPIC value for unknown channel %s",
+ line.Args[1])
+ }
+}
+
+// Handle 352 who reply
+func (conn *Conn) h_352(line *Line) {
+ if !line.argslen(5) {
+ return
+ }
+ nk := conn.st.GetNick(line.Args[5])
+ if nk == nil {
+ logging.Warn("irc.352(): received WHO reply for unknown nick %s",
+ line.Args[5])
+ return
+ }
+ if conn.Me().Equals(nk) {
+ return
+ }
+ // XXX: do we care about the actual server the nick is on?
+ // or the hop count to this server?
+ // last arg contains "<hop count> <real name>"
+ a := strings.SplitN(line.Args[len(line.Args)-1], " ", 2)
+ conn.st.NickInfo(nk.Nick, line.Args[2], line.Args[3], a[1])
+ if !line.argslen(6) {
+ return
+ }
+ if idx := strings.Index(line.Args[6], "*"); idx != -1 {
+ conn.st.NickModes(nk.Nick, "+o")
+ }
+ if idx := strings.Index(line.Args[6], "B"); idx != -1 {
+ conn.st.NickModes(nk.Nick, "+B")
+ }
+ if idx := strings.Index(line.Args[6], "H"); idx != -1 {
+ conn.st.NickModes(nk.Nick, "+i")
+ }
+}
+
+// Handle 353 names reply
+func (conn *Conn) h_353(line *Line) {
+ if !line.argslen(2) {
+ return
+ }
+ if ch := conn.st.GetChannel(line.Args[2]); ch != nil {
+ nicks := strings.Split(line.Args[len(line.Args)-1], " ")
+ for _, nick := range nicks {
+ // UnrealIRCd's coders are lazy and leave a trailing space
+ if nick == "" {
+ continue
+ }
+ switch c := nick[0]; c {
+ case '~', '&', '@', '%', '+':
+ nick = nick[1:]
+ fallthrough
+ default:
+ if conn.st.GetNick(nick) == nil {
+ // we don't know this nick yet!
+ conn.st.NewNick(nick)
+ }
+ if _, ok := conn.st.IsOn(ch.Name, nick); !ok {
+ // This nick isn't associated with this channel yet!
+ conn.st.Associate(ch.Name, nick)
+ }
+ switch c {
+ case '~':
+ conn.st.ChannelModes(ch.Name, "+q", nick)
+ case '&':
+ conn.st.ChannelModes(ch.Name, "+a", nick)
+ case '@':
+ conn.st.ChannelModes(ch.Name, "+o", nick)
+ case '%':
+ conn.st.ChannelModes(ch.Name, "+h", nick)
+ case '+':
+ conn.st.ChannelModes(ch.Name, "+v", nick)
+ }
+ }
+ }
+ } else {
+ logging.Warn("irc.353(): received NAMES list for unknown channel %s",
+ line.Args[2])
+ }
+}
+
+// Handle 671 whois reply (nick connected via SSL)
+func (conn *Conn) h_671(line *Line) {
+ if !line.argslen(1) {
+ return
+ }
+ if nk := conn.st.GetNick(line.Args[1]); nk != nil {
+ conn.st.NickModes(nk.Nick, "+z")
+ } else {
+ logging.Warn("irc.671(): received WHOIS SSL info for unknown nick %s",
+ line.Args[1])
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/logging/logging.go b/vendor/github.com/fluffle/goirc/logging/logging.go
new file mode 100644
index 0000000..bd54416
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/logging/logging.go
@@ -0,0 +1,43 @@
+package logging
+
+// The IRC client will log things using these methods
+type Logger interface {
+ // Debug logging of raw socket comms to/from server.
+ Debug(format string, args ...interface{})
+ // Informational logging about client behaviour.
+ Info(format string, args ...interface{})
+ // Warnings of inconsistent or unexpected data, mostly
+ // related to state tracking of IRC nicks/chans.
+ Warn(format string, args ...interface{})
+ // Errors, mostly to do with network communication.
+ Error(format string, args ...interface{})
+}
+
+// By default we do no logging. Logging is enabled or disabled
+// at the package level, since I'm lazy and re-re-reorganising
+// my code to pass a per-client-struct Logger around to all the
+// state objects is a pain in the arse.
+var logger Logger = nullLogger{}
+
+// SetLogger sets the internal goirc Logger to l. If l is nil,
+// a dummy logger that does nothing is installed instead.
+func SetLogger(l Logger) {
+ if l == nil {
+ logger = nullLogger{}
+ } else {
+ logger = l
+ }
+}
+
+// A nullLogger does nothing while fulfilling Logger.
+type nullLogger struct{}
+func (nl nullLogger) Debug(f string, a ...interface{}) {}
+func (nl nullLogger) Info(f string, a ...interface{}) {}
+func (nl nullLogger) Warn(f string, a ...interface{}) {}
+func (nl nullLogger) Error(f string, a ...interface{}) {}
+
+// Shim functions so that the package can be used directly
+func Debug(f string, a ...interface{}) { logger.Debug(f, a...) }
+func Info(f string, a ...interface{}) { logger.Info(f, a...) }
+func Warn(f string, a ...interface{}) { logger.Warn(f, a...) }
+func Error(f string, a ...interface{}) { logger.Error(f, a...) }
diff --git a/vendor/github.com/fluffle/goirc/state/channel.go b/vendor/github.com/fluffle/goirc/state/channel.go
new file mode 100644
index 0000000..35379d2
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/state/channel.go
@@ -0,0 +1,350 @@
+package state
+
+import (
+ "github.com/fluffle/goirc/logging"
+
+ "reflect"
+ "strconv"
+)
+
+// A Channel is returned from the state tracker and contains
+// a copy of the channel state at a particular time.
+type Channel struct {
+ Name, Topic string
+ Modes *ChanMode
+ Nicks map[string]*ChanPrivs
+}
+
+// Internal bookkeeping struct for channels.
+type channel struct {
+ name, topic string
+ modes *ChanMode
+ lookup map[string]*nick
+ nicks map[*nick]*ChanPrivs
+}
+
+// A struct representing the modes of an IRC Channel
+// (the ones we care about, at least).
+// http://www.unrealircd.com/files/docs/unreal32docs.html#userchannelmodes
+type ChanMode struct {
+ // MODE +p, +s, +t, +n, +m
+ Private, Secret, ProtectedTopic, NoExternalMsg, Moderated bool
+
+ // MODE +i, +O, +z
+ InviteOnly, OperOnly, SSLOnly bool
+
+ // MODE +r, +Z
+ Registered, AllSSL bool
+
+ // MODE +k
+ Key string
+
+ // MODE +l
+ Limit int
+}
+
+// A struct representing the modes a Nick can have on a Channel
+type ChanPrivs struct {
+ // MODE +q, +a, +o, +h, +v
+ Owner, Admin, Op, HalfOp, Voice bool
+}
+
+// Map ChanMode fields to IRC mode characters
+var StringToChanMode = map[string]string{}
+var ChanModeToString = map[string]string{
+ "Private": "p",
+ "Secret": "s",
+ "ProtectedTopic": "t",
+ "NoExternalMsg": "n",
+ "Moderated": "m",
+ "InviteOnly": "i",
+ "OperOnly": "O",
+ "SSLOnly": "z",
+ "Registered": "r",
+ "AllSSL": "Z",
+ "Key": "k",
+ "Limit": "l",
+}
+
+// Map *irc.ChanPrivs fields to IRC mode characters
+var StringToChanPriv = map[string]string{}
+var ChanPrivToString = map[string]string{
+ "Owner": "q",
+ "Admin": "a",
+ "Op": "o",
+ "HalfOp": "h",
+ "Voice": "v",
+}
+
+// Map *irc.ChanPrivs fields to the symbols used to represent these modes
+// in NAMES and WHOIS responses
+var ModeCharToChanPriv = map[byte]string{}
+var ChanPrivToModeChar = map[string]byte{
+ "Owner": '~',
+ "Admin": '&',
+ "Op": '@',
+ "HalfOp": '%',
+ "Voice": '+',
+}
+
+// Init function to fill in reverse mappings for *toString constants.
+func init() {
+ for k, v := range ChanModeToString {
+ StringToChanMode[v] = k
+ }
+ for k, v := range ChanPrivToString {
+ StringToChanPriv[v] = k
+ }
+ for k, v := range ChanPrivToModeChar {
+ ModeCharToChanPriv[v] = k
+ }
+}
+
+/******************************************************************************\
+ * Channel methods for state management
+\******************************************************************************/
+
+func newChannel(name string) *channel {
+ return &channel{
+ name: name,
+ modes: new(ChanMode),
+ nicks: make(map[*nick]*ChanPrivs),
+ lookup: make(map[string]*nick),
+ }
+}
+
+// Returns a copy of the internal tracker channel state at this time.
+// Relies on tracker-level locking for concurrent access.
+func (ch *channel) Channel() *Channel {
+ c := &Channel{
+ Name: ch.name,
+ Topic: ch.topic,
+ Modes: ch.modes.Copy(),
+ Nicks: make(map[string]*ChanPrivs),
+ }
+ for n, cp := range ch.nicks {
+ c.Nicks[n.nick] = cp.Copy()
+ }
+ return c
+}
+
+func (ch *channel) isOn(nk *nick) (*ChanPrivs, bool) {
+ cp, ok := ch.nicks[nk]
+ return cp.Copy(), ok
+}
+
+// Associates a Nick with a Channel
+func (ch *channel) addNick(nk *nick, cp *ChanPrivs) {
+ if _, ok := ch.nicks[nk]; !ok {
+ ch.nicks[nk] = cp
+ ch.lookup[nk.nick] = nk
+ } else {
+ logging.Warn("Channel.addNick(): %s already on %s.", nk.nick, ch.name)
+ }
+}
+
+// Disassociates a Nick from a Channel.
+func (ch *channel) delNick(nk *nick) {
+ if _, ok := ch.nicks[nk]; ok {
+ delete(ch.nicks, nk)
+ delete(ch.lookup, nk.nick)
+ } else {
+ logging.Warn("Channel.delNick(): %s not on %s.", nk.nick, ch.name)
+ }
+}
+
+// Parses mode strings for a channel.
+func (ch *channel) parseModes(modes string, modeargs ...string) {
+ var modeop bool // true => add mode, false => remove mode
+ var modestr string
+ for i := 0; i < len(modes); i++ {
+ switch m := modes[i]; m {
+ case '+':
+ modeop = true
+ modestr = string(m)
+ case '-':
+ modeop = false
+ modestr = string(m)
+ case 'i':
+ ch.modes.InviteOnly = modeop
+ case 'm':
+ ch.modes.Moderated = modeop
+ case 'n':
+ ch.modes.NoExternalMsg = modeop
+ case 'p':
+ ch.modes.Private = modeop
+ case 'r':
+ ch.modes.Registered = modeop
+ case 's':
+ ch.modes.Secret = modeop
+ case 't':
+ ch.modes.ProtectedTopic = modeop
+ case 'z':
+ ch.modes.SSLOnly = modeop
+ case 'Z':
+ ch.modes.AllSSL = modeop
+ case 'O':
+ ch.modes.OperOnly = modeop
+ case 'k':
+ if modeop && len(modeargs) != 0 {
+ ch.modes.Key, modeargs = modeargs[0], modeargs[1:]
+ } else if !modeop {
+ ch.modes.Key = ""
+ } else {
+ logging.Warn("Channel.ParseModes(): not enough arguments to "+
+ "process MODE %s %s%c", ch.name, modestr, m)
+ }
+ case 'l':
+ if modeop && len(modeargs) != 0 {
+ ch.modes.Limit, _ = strconv.Atoi(modeargs[0])
+ modeargs = modeargs[1:]
+ } else if !modeop {
+ ch.modes.Limit = 0
+ } else {
+ logging.Warn("Channel.ParseModes(): not enough arguments to "+
+ "process MODE %s %s%c", ch.name, modestr, m)
+ }
+ case 'q', 'a', 'o', 'h', 'v':
+ if len(modeargs) != 0 {
+ if nk, ok := ch.lookup[modeargs[0]]; ok {
+ cp := ch.nicks[nk]
+ switch m {
+ case 'q':
+ cp.Owner = modeop
+ case 'a':
+ cp.Admin = modeop
+ case 'o':
+ cp.Op = modeop
+ case 'h':
+ cp.HalfOp = modeop
+ case 'v':
+ cp.Voice = modeop
+ }
+ modeargs = modeargs[1:]
+ } else {
+ logging.Warn("Channel.ParseModes(): untracked nick %s "+
+ "received MODE on channel %s", modeargs[0], ch.name)
+ }
+ } else {
+ logging.Warn("Channel.ParseModes(): not enough arguments to "+
+ "process MODE %s %s%c", ch.name, modestr, m)
+ }
+ default:
+ logging.Info("Channel.ParseModes(): unknown mode char %c", m)
+ }
+ }
+}
+
+// Returns true if the Nick is associated with the Channel
+func (ch *Channel) IsOn(nk string) (*ChanPrivs, bool) {
+ cp, ok := ch.Nicks[nk]
+ return cp, ok
+}
+
+// Test Channel equality.
+func (ch *Channel) Equals(other *Channel) bool {
+ return reflect.DeepEqual(ch, other)
+}
+
+// Duplicates a ChanMode struct.
+func (cm *ChanMode) Copy() *ChanMode {
+ if cm == nil { return nil }
+ c := *cm
+ return &c
+}
+
+// Test ChanMode equality.
+func (cm *ChanMode) Equals(other *ChanMode) bool {
+ return reflect.DeepEqual(cm, other)
+}
+
+// Duplicates a ChanPrivs struct.
+func (cp *ChanPrivs) Copy() *ChanPrivs {
+ if cp == nil { return nil }
+ c := *cp
+ return &c
+}
+
+// Test ChanPrivs equality.
+func (cp *ChanPrivs) Equals(other *ChanPrivs) bool {
+ return reflect.DeepEqual(cp, other)
+}
+
+// Returns a string representing the channel. Looks like:
+// Channel: <channel name> e.g. #moo
+// Topic: <channel topic> e.g. Discussing the merits of cows!
+// Mode: <channel modes> e.g. +nsti
+// Nicks:
+// <nick>: <privs> e.g. CowMaster: +o
+// ...
+func (ch *Channel) String() string {
+ str := "Channel: " + ch.Name + "\n\t"
+ str += "Topic: " + ch.Topic + "\n\t"
+ str += "Modes: " + ch.Modes.String() + "\n\t"
+ str += "Nicks: \n"
+ for nk, cp := range ch.Nicks {
+ str += "\t\t" + nk + ": " + cp.String() + "\n"
+ }
+ return str
+}
+
+func (ch *channel) String() string {
+ return ch.Channel().String()
+}
+
+// Returns a string representing the channel modes. Looks like:
+// +npk key
+func (cm *ChanMode) String() string {
+ str := "+"
+ a := make([]string, 0)
+ v := reflect.Indirect(reflect.ValueOf(cm))
+ t := v.Type()
+ for i := 0; i < v.NumField(); i++ {
+ switch f := v.Field(i); f.Kind() {
+ case reflect.Bool:
+ if f.Bool() {
+ str += ChanModeToString[t.Field(i).Name]
+ }
+ case reflect.String:
+ if f.String() != "" {
+ str += ChanModeToString[t.Field(i).Name]
+ a = append(a, f.String())
+ }
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ if f.Int() != 0 {
+ str += ChanModeToString[t.Field(i).Name]
+ a = append(a, strconv.FormatInt(f.Int(), 10))
+ }
+ }
+ }
+ for _, s := range a {
+ if s != "" {
+ str += " " + s
+ }
+ }
+ if str == "+" {
+ str = "No modes set"
+ }
+ return str
+}
+
+// Returns a string representing the channel privileges. Looks like:
+// +o
+func (cp *ChanPrivs) String() string {
+ str := "+"
+ v := reflect.Indirect(reflect.ValueOf(cp))
+ t := v.Type()
+ for i := 0; i < v.NumField(); i++ {
+ switch f := v.Field(i); f.Kind() {
+ // only bools here at the mo too!
+ case reflect.Bool:
+ if f.Bool() {
+ str += ChanPrivToString[t.Field(i).Name]
+ }
+ }
+ }
+ if str == "+" {
+ str = "No modes set"
+ }
+ return str
+}
diff --git a/vendor/github.com/fluffle/goirc/state/channel_test.go b/vendor/github.com/fluffle/goirc/state/channel_test.go
new file mode 100644
index 0000000..d5c804d
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/state/channel_test.go
@@ -0,0 +1,176 @@
+package state
+
+import "testing"
+
+func compareChannel(t *testing.T, ch *channel) {
+ c := ch.Channel()
+ if c.Name != ch.name || c.Topic != ch.topic ||
+ !c.Modes.Equals(ch.modes) || len(c.Nicks) != len(ch.nicks) {
+ t.Errorf("Channel not duped correctly from internal state.")
+ }
+ for nk, cp := range ch.nicks {
+ if other, ok := c.Nicks[nk.nick]; !ok || !cp.Equals(other) {
+ t.Errorf("Nick not duped correctly from internal state.")
+ }
+ }
+}
+
+func TestNewChannel(t *testing.T) {
+ ch := newChannel("#test1")
+
+ if ch.name != "#test1" {
+ t.Errorf("Channel not created correctly by NewChannel()")
+ }
+ if len(ch.nicks) != 0 || len(ch.lookup) != 0 {
+ t.Errorf("Channel maps contain data after NewChannel()")
+ }
+ compareChannel(t, ch)
+}
+
+func TestAddNick(t *testing.T) {
+ ch := newChannel("#test1")
+ nk := newNick("test1")
+ cp := new(ChanPrivs)
+
+ ch.addNick(nk, cp)
+
+ if len(ch.nicks) != 1 || len(ch.lookup) != 1 {
+ t.Errorf("Nick lists not updated correctly for add.")
+ }
+ if c, ok := ch.nicks[nk]; !ok || c != cp {
+ t.Errorf("Nick test1 not properly stored in nicks map.")
+ }
+ if n, ok := ch.lookup["test1"]; !ok || n != nk {
+ t.Errorf("Nick test1 not properly stored in lookup map.")
+ }
+ compareChannel(t, ch)
+}
+
+func TestDelNick(t *testing.T) {
+ ch := newChannel("#test1")
+ nk := newNick("test1")
+ cp := new(ChanPrivs)
+
+ ch.addNick(nk, cp)
+ ch.delNick(nk)
+ if len(ch.nicks) != 0 || len(ch.lookup) != 0 {
+ t.Errorf("Nick lists not updated correctly for del.")
+ }
+ if c, ok := ch.nicks[nk]; ok || c != nil {
+ t.Errorf("Nick test1 not properly removed from nicks map.")
+ }
+ if n, ok := ch.lookup["#test1"]; ok || n != nil {
+ t.Errorf("Nick test1 not properly removed from lookup map.")
+ }
+ compareChannel(t, ch)
+}
+
+func TestChannelParseModes(t *testing.T) {
+ ch := newChannel("#test1")
+ md := ch.modes
+
+ // Channel modes can adjust channel privs too, so we need a Nick
+ nk := newNick("test1")
+ cp := new(ChanPrivs)
+ ch.addNick(nk, cp)
+
+ // Test bools first.
+ compareChannel(t, ch)
+ if md.Private || md.Secret || md.ProtectedTopic || md.NoExternalMsg ||
+ md.Moderated || md.InviteOnly || md.OperOnly || md.SSLOnly {
+ t.Errorf("Modes for new channel set to true.")
+ }
+
+ // Flip some bits!
+ md.Private = true
+ md.NoExternalMsg = true
+ md.InviteOnly = true
+
+ // Flip some MOAR bits.
+ ch.parseModes("+s-p+tm-i")
+
+ compareChannel(t, ch)
+ if md.Private || !md.Secret || !md.ProtectedTopic || !md.NoExternalMsg ||
+ !md.Moderated || md.InviteOnly || md.OperOnly || md.SSLOnly {
+ t.Errorf("Modes not flipped correctly by ParseModes.")
+ }
+
+ // Test numeric parsing (currently only channel limits)
+ if md.Limit != 0 {
+ t.Errorf("Limit for new channel not zero.")
+ }
+
+ // enable limit correctly
+ ch.parseModes("+l", "256")
+ compareChannel(t, ch)
+ if md.Limit != 256 {
+ t.Errorf("Limit for channel not set correctly")
+ }
+
+ // enable limit incorrectly
+ ch.parseModes("+l")
+ compareChannel(t, ch)
+ if md.Limit != 256 {
+ t.Errorf("Bad limit value caused limit to be unset.")
+ }
+
+ // disable limit correctly
+ ch.parseModes("-l")
+ compareChannel(t, ch)
+ if md.Limit != 0 {
+ t.Errorf("Limit for channel not unset correctly")
+ }
+
+ // Test string parsing (currently only channel key)
+ if md.Key != "" {
+ t.Errorf("Key set for new channel.")
+ }
+
+ // enable key correctly
+ ch.parseModes("+k", "foobar")
+ compareChannel(t, ch)
+ if md.Key != "foobar" {
+ t.Errorf("Key for channel not set correctly")
+ }
+
+ // enable key incorrectly
+ ch.parseModes("+k")
+ compareChannel(t, ch)
+ if md.Key != "foobar" {
+ t.Errorf("Bad key value caused key to be unset.")
+ }
+
+ // disable key correctly
+ ch.parseModes("-k")
+ compareChannel(t, ch)
+ if md.Key != "" {
+ t.Errorf("Key for channel not unset correctly")
+ }
+
+ // Test chan privs parsing.
+ cp.Op = true
+ cp.HalfOp = true
+ ch.parseModes("+aq-o", "test1", "test1", "test1")
+
+ compareChannel(t, ch)
+ if !cp.Owner || !cp.Admin || cp.Op || !cp.HalfOp || cp.Voice {
+ t.Errorf("Channel privileges not flipped correctly by ParseModes.")
+ }
+
+ // Test a random mix of modes, just to be sure
+ md.Limit = 256
+ ch.parseModes("+zpt-qsl+kv-h", "test1", "foobar", "test1")
+
+ compareChannel(t, ch)
+ if !md.Private || md.Secret || !md.ProtectedTopic || !md.NoExternalMsg ||
+ !md.Moderated || md.InviteOnly || md.OperOnly || !md.SSLOnly {
+ t.Errorf("Modes not flipped correctly by ParseModes (2).")
+ }
+ if md.Limit != 0 || md.Key != "foobar" {
+ t.Errorf("Key and limit not changed correctly by ParseModes (2).")
+ }
+ if cp.Owner || !cp.Admin || cp.Op || !cp.HalfOp || !cp.Voice {
+ // NOTE: HalfOp not actually unset above thanks to deliberate error.
+ t.Errorf("Channel privileges not flipped correctly by ParseModes (2).")
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/state/mock_tracker.go b/vendor/github.com/fluffle/goirc/state/mock_tracker.go
new file mode 100644
index 0000000..aa594c4
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/state/mock_tracker.go
@@ -0,0 +1,201 @@
+// Automatically generated by MockGen. DO NOT EDIT!
+// Source: tracker.go
+
+package state
+
+import (
+ gomock "github.com/golang/mock/gomock"
+)
+
+// Mock of Tracker interface
+type MockTracker struct {
+ ctrl *gomock.Controller
+ recorder *_MockTrackerRecorder
+}
+
+// Recorder for MockTracker (not exported)
+type _MockTrackerRecorder struct {
+ mock *MockTracker
+}
+
+func NewMockTracker(ctrl *gomock.Controller) *MockTracker {
+ mock := &MockTracker{ctrl: ctrl}
+ mock.recorder = &_MockTrackerRecorder{mock}
+ return mock
+}
+
+func (_m *MockTracker) EXPECT() *_MockTrackerRecorder {
+ return _m.recorder
+}
+
+func (_m *MockTracker) NewNick(nick string) *Nick {
+ ret := _m.ctrl.Call(_m, "NewNick", nick)
+ ret0, _ := ret[0].(*Nick)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) NewNick(arg0 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "NewNick", arg0)
+}
+
+func (_m *MockTracker) GetNick(nick string) *Nick {
+ ret := _m.ctrl.Call(_m, "GetNick", nick)
+ ret0, _ := ret[0].(*Nick)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) GetNick(arg0 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "GetNick", arg0)
+}
+
+func (_m *MockTracker) ReNick(old string, neu string) *Nick {
+ ret := _m.ctrl.Call(_m, "ReNick", old, neu)
+ ret0, _ := ret[0].(*Nick)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) ReNick(arg0, arg1 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "ReNick", arg0, arg1)
+}
+
+func (_m *MockTracker) DelNick(nick string) *Nick {
+ ret := _m.ctrl.Call(_m, "DelNick", nick)
+ ret0, _ := ret[0].(*Nick)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) DelNick(arg0 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "DelNick", arg0)
+}
+
+func (_m *MockTracker) NickInfo(nick string, ident string, host string, name string) *Nick {
+ ret := _m.ctrl.Call(_m, "NickInfo", nick, ident, host, name)
+ ret0, _ := ret[0].(*Nick)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) NickInfo(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "NickInfo", arg0, arg1, arg2, arg3)
+}
+
+func (_m *MockTracker) NickModes(nick string, modestr string) *Nick {
+ ret := _m.ctrl.Call(_m, "NickModes", nick, modestr)
+ ret0, _ := ret[0].(*Nick)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) NickModes(arg0, arg1 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "NickModes", arg0, arg1)
+}
+
+func (_m *MockTracker) NewChannel(channel string) *Channel {
+ ret := _m.ctrl.Call(_m, "NewChannel", channel)
+ ret0, _ := ret[0].(*Channel)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) NewChannel(arg0 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "NewChannel", arg0)
+}
+
+func (_m *MockTracker) GetChannel(channel string) *Channel {
+ ret := _m.ctrl.Call(_m, "GetChannel", channel)
+ ret0, _ := ret[0].(*Channel)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) GetChannel(arg0 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "GetChannel", arg0)
+}
+
+func (_m *MockTracker) DelChannel(channel string) *Channel {
+ ret := _m.ctrl.Call(_m, "DelChannel", channel)
+ ret0, _ := ret[0].(*Channel)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) DelChannel(arg0 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "DelChannel", arg0)
+}
+
+func (_m *MockTracker) Topic(channel string, topic string) *Channel {
+ ret := _m.ctrl.Call(_m, "Topic", channel, topic)
+ ret0, _ := ret[0].(*Channel)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) Topic(arg0, arg1 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "Topic", arg0, arg1)
+}
+
+func (_m *MockTracker) ChannelModes(channel string, modestr string, modeargs ...string) *Channel {
+ _s := []interface{}{channel, modestr}
+ for _, _x := range modeargs {
+ _s = append(_s, _x)
+ }
+ ret := _m.ctrl.Call(_m, "ChannelModes", _s...)
+ ret0, _ := ret[0].(*Channel)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) ChannelModes(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ _s := append([]interface{}{arg0, arg1}, arg2...)
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "ChannelModes", _s...)
+}
+
+func (_m *MockTracker) Me() *Nick {
+ ret := _m.ctrl.Call(_m, "Me")
+ ret0, _ := ret[0].(*Nick)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) Me() *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "Me")
+}
+
+func (_m *MockTracker) IsOn(channel string, nick string) (*ChanPrivs, bool) {
+ ret := _m.ctrl.Call(_m, "IsOn", channel, nick)
+ ret0, _ := ret[0].(*ChanPrivs)
+ ret1, _ := ret[1].(bool)
+ return ret0, ret1
+}
+
+func (_mr *_MockTrackerRecorder) IsOn(arg0, arg1 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "IsOn", arg0, arg1)
+}
+
+func (_m *MockTracker) Associate(channel string, nick string) *ChanPrivs {
+ ret := _m.ctrl.Call(_m, "Associate", channel, nick)
+ ret0, _ := ret[0].(*ChanPrivs)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) Associate(arg0, arg1 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "Associate", arg0, arg1)
+}
+
+func (_m *MockTracker) Dissociate(channel string, nick string) {
+ _m.ctrl.Call(_m, "Dissociate", channel, nick)
+}
+
+func (_mr *_MockTrackerRecorder) Dissociate(arg0, arg1 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "Dissociate", arg0, arg1)
+}
+
+func (_m *MockTracker) Wipe() {
+ _m.ctrl.Call(_m, "Wipe")
+}
+
+func (_mr *_MockTrackerRecorder) Wipe() *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "Wipe")
+}
+
+func (_m *MockTracker) String() string {
+ ret := _m.ctrl.Call(_m, "String")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) String() *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "String")
+}
diff --git a/vendor/github.com/fluffle/goirc/state/nick.go b/vendor/github.com/fluffle/goirc/state/nick.go
new file mode 100644
index 0000000..7704b0b
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/state/nick.go
@@ -0,0 +1,200 @@
+package state
+
+import (
+ "github.com/fluffle/goirc/logging"
+
+ "reflect"
+)
+
+// A Nick is returned from the state tracker and contains
+// a copy of the nick state at a particular time.
+type Nick struct {
+ Nick, Ident, Host, Name string
+ Modes *NickMode
+ Channels map[string]*ChanPrivs
+}
+
+// Internal bookkeeping struct for nicks.
+type nick struct {
+ nick, ident, host, name string
+ modes *NickMode
+ lookup map[string]*channel
+ chans map[*channel]*ChanPrivs
+}
+
+// A struct representing the modes of an IRC Nick (User Modes)
+// (again, only the ones we care about)
+//
+// This is only really useful for me, as we can't see other people's modes
+// without IRC operator privileges (and even then only on some IRCd's).
+type NickMode struct {
+ // MODE +B, +i, +o, +w, +x, +z
+ Bot, Invisible, Oper, WallOps, HiddenHost, SSL bool
+}
+
+// Map *irc.NickMode fields to IRC mode characters and vice versa
+var StringToNickMode = map[string]string{}
+var NickModeToString = map[string]string{
+ "Bot": "B",
+ "Invisible": "i",
+ "Oper": "o",
+ "WallOps": "w",
+ "HiddenHost": "x",
+ "SSL": "z",
+}
+
+func init() {
+ for k, v := range NickModeToString {
+ StringToNickMode[v] = k
+ }
+}
+
+/******************************************************************************\
+ * nick methods for state management
+\******************************************************************************/
+
+func newNick(n string) *nick {
+ return &nick{
+ nick: n,
+ modes: new(NickMode),
+ chans: make(map[*channel]*ChanPrivs),
+ lookup: make(map[string]*channel),
+ }
+}
+
+// Returns a copy of the internal tracker nick state at this time.
+// Relies on tracker-level locking for concurrent access.
+func (nk *nick) Nick() *Nick {
+ n := &Nick{
+ Nick: nk.nick,
+ Ident: nk.ident,
+ Host: nk.host,
+ Name: nk.name,
+ Modes: nk.modes.Copy(),
+ Channels: make(map[string]*ChanPrivs),
+ }
+ for c, cp := range nk.chans {
+ n.Channels[c.name] = cp.Copy()
+ }
+ return n
+}
+
+func (nk *nick) isOn(ch *channel) (*ChanPrivs, bool) {
+ cp, ok := nk.chans[ch]
+ return cp.Copy(), ok
+}
+
+// Associates a Channel with a Nick.
+func (nk *nick) addChannel(ch *channel, cp *ChanPrivs) {
+ if _, ok := nk.chans[ch]; !ok {
+ nk.chans[ch] = cp
+ nk.lookup[ch.name] = ch
+ } else {
+ logging.Warn("Nick.addChannel(): %s already on %s.", nk.nick, ch.name)
+ }
+}
+
+// Disassociates a Channel from a Nick.
+func (nk *nick) delChannel(ch *channel) {
+ if _, ok := nk.chans[ch]; ok {
+ delete(nk.chans, ch)
+ delete(nk.lookup, ch.name)
+ } else {
+ logging.Warn("Nick.delChannel(): %s not on %s.", nk.nick, ch.name)
+ }
+}
+
+// Parse mode strings for a Nick.
+func (nk *nick) parseModes(modes string) {
+ var modeop bool // true => add mode, false => remove mode
+ for i := 0; i < len(modes); i++ {
+ switch m := modes[i]; m {
+ case '+':
+ modeop = true
+ case '-':
+ modeop = false
+ case 'B':
+ nk.modes.Bot = modeop
+ case 'i':
+ nk.modes.Invisible = modeop
+ case 'o':
+ nk.modes.Oper = modeop
+ case 'w':
+ nk.modes.WallOps = modeop
+ case 'x':
+ nk.modes.HiddenHost = modeop
+ case 'z':
+ nk.modes.SSL = modeop
+ default:
+ logging.Info("Nick.ParseModes(): unknown mode char %c", m)
+ }
+ }
+}
+
+// Returns true if the Nick is associated with the Channel.
+func (nk *Nick) IsOn(ch string) (*ChanPrivs, bool) {
+ cp, ok := nk.Channels[ch]
+ return cp, ok
+}
+
+// Tests Nick equality.
+func (nk *Nick) Equals(other *Nick) bool {
+ return reflect.DeepEqual(nk, other)
+}
+
+// Duplicates a NickMode struct.
+func (nm *NickMode) Copy() *NickMode {
+ if nm == nil { return nil }
+ n := *nm
+ return &n
+}
+
+// Tests NickMode equality.
+func (nm *NickMode) Equals(other *NickMode) bool {
+ return reflect.DeepEqual(nm, other)
+}
+
+// Returns a string representing the nick. Looks like:
+// Nick: <nick name> e.g. CowMaster
+// Hostmask: <ident@host> e.g. moo@cows.org
+// Real Name: <real name> e.g. Steve "CowMaster" Bush
+// Modes: <nick modes> e.g. +z
+// Channels:
+// <channel>: <privs> e.g. #moo: +o
+// ...
+func (nk *Nick) String() string {
+ str := "Nick: " + nk.Nick + "\n\t"
+ str += "Hostmask: " + nk.Ident + "@" + nk.Host + "\n\t"
+ str += "Real Name: " + nk.Name + "\n\t"
+ str += "Modes: " + nk.Modes.String() + "\n\t"
+ str += "Channels: \n"
+ for ch, cp := range nk.Channels {
+ str += "\t\t" + ch + ": " + cp.String() + "\n"
+ }
+ return str
+}
+
+func (nk *nick) String() string {
+ return nk.Nick().String()
+}
+
+// Returns a string representing the nick modes. Looks like:
+// +iwx
+func (nm *NickMode) String() string {
+ str := "+"
+ v := reflect.Indirect(reflect.ValueOf(nm))
+ t := v.Type()
+ for i := 0; i < v.NumField(); i++ {
+ switch f := v.Field(i); f.Kind() {
+ // only bools here at the mo!
+ case reflect.Bool:
+ if f.Bool() {
+ str += NickModeToString[t.Field(i).Name]
+ }
+ }
+ }
+ if str == "+" {
+ str = "No modes set"
+ }
+ return str
+}
diff --git a/vendor/github.com/fluffle/goirc/state/nick_test.go b/vendor/github.com/fluffle/goirc/state/nick_test.go
new file mode 100644
index 0000000..1344400
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/state/nick_test.go
@@ -0,0 +1,88 @@
+package state
+
+import "testing"
+
+func compareNick(t *testing.T, nk *nick) {
+ n := nk.Nick()
+ if n.Nick != nk.nick || n.Ident != nk.ident || n.Host != nk.host || n.Name != nk.name ||
+ !n.Modes.Equals(nk.modes) || len(n.Channels) != len(nk.chans) {
+ t.Errorf("Nick not duped correctly from internal state.")
+ }
+ for ch, cp := range nk.chans {
+ if other, ok := n.Channels[ch.name]; !ok || !cp.Equals(other) {
+ t.Errorf("Channel not duped correctly from internal state.")
+ }
+ }
+}
+
+func TestNewNick(t *testing.T) {
+ nk := newNick("test1")
+
+ if nk.nick != "test1" {
+ t.Errorf("Nick not created correctly by NewNick()")
+ }
+ if len(nk.chans) != 0 || len(nk.lookup) != 0 {
+ t.Errorf("Nick maps contain data after NewNick()")
+ }
+ compareNick(t, nk)
+}
+
+func TestAddChannel(t *testing.T) {
+ nk := newNick("test1")
+ ch := newChannel("#test1")
+ cp := new(ChanPrivs)
+
+ nk.addChannel(ch, cp)
+
+ if len(nk.chans) != 1 || len(nk.lookup) != 1 {
+ t.Errorf("Channel lists not updated correctly for add.")
+ }
+ if c, ok := nk.chans[ch]; !ok || c != cp {
+ t.Errorf("Channel #test1 not properly stored in chans map.")
+ }
+ if c, ok := nk.lookup["#test1"]; !ok || c != ch {
+ t.Errorf("Channel #test1 not properly stored in lookup map.")
+ }
+ compareNick(t, nk)
+}
+
+func TestDelChannel(t *testing.T) {
+ nk := newNick("test1")
+ ch := newChannel("#test1")
+ cp := new(ChanPrivs)
+
+ nk.addChannel(ch, cp)
+ nk.delChannel(ch)
+ if len(nk.chans) != 0 || len(nk.lookup) != 0 {
+ t.Errorf("Channel lists not updated correctly for del.")
+ }
+ if c, ok := nk.chans[ch]; ok || c != nil {
+ t.Errorf("Channel #test1 not properly removed from chans map.")
+ }
+ if c, ok := nk.lookup["#test1"]; ok || c != nil {
+ t.Errorf("Channel #test1 not properly removed from lookup map.")
+ }
+ compareNick(t, nk)
+}
+
+func TestNickParseModes(t *testing.T) {
+ nk := newNick("test1")
+ md := nk.modes
+
+ // Modes should all be false for a new nick
+ if md.Invisible || md.Oper || md.WallOps || md.HiddenHost || md.SSL {
+ t.Errorf("Modes for new nick set to true.")
+ }
+
+ // Set a couple of modes, for testing.
+ md.Invisible = true
+ md.HiddenHost = true
+
+ // Parse a mode line that flips one true to false and two false to true
+ nk.parseModes("+z-x+w")
+
+ compareNick(t, nk)
+ if !md.Invisible || md.Oper || !md.WallOps || md.HiddenHost || !md.SSL {
+ t.Errorf("Modes not flipped correctly by ParseModes.")
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/state/tracker.go b/vendor/github.com/fluffle/goirc/state/tracker.go
new file mode 100644
index 0000000..209c7cf
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/state/tracker.go
@@ -0,0 +1,369 @@
+package state
+
+import (
+ "github.com/fluffle/goirc/logging"
+
+ "sync"
+)
+
+// The state manager interface
+type Tracker interface {
+ // Nick methods
+ NewNick(nick string) *Nick
+ GetNick(nick string) *Nick
+ ReNick(old, neu string) *Nick
+ DelNick(nick string) *Nick
+ NickInfo(nick, ident, host, name string) *Nick
+ NickModes(nick, modestr string) *Nick
+ // Channel methods
+ NewChannel(channel string) *Channel
+ GetChannel(channel string) *Channel
+ DelChannel(channel string) *Channel
+ Topic(channel, topic string) *Channel
+ ChannelModes(channel, modestr string, modeargs ...string) *Channel
+ // Information about ME!
+ Me() *Nick
+ // And the tracking operations
+ IsOn(channel, nick string) (*ChanPrivs, bool)
+ Associate(channel, nick string) *ChanPrivs
+ Dissociate(channel, nick string)
+ Wipe()
+ // The state tracker can output a debugging string
+ String() string
+}
+
+// ... and a struct to implement it ...
+type stateTracker struct {
+ // Map of channels we're on
+ chans map[string]*channel
+ // Map of nicks we know about
+ nicks map[string]*nick
+
+ // We need to keep state on who we are :-)
+ me *nick
+
+ // And we need to protect against data races *cough*.
+ mu sync.Mutex
+}
+
+var _ Tracker = (*stateTracker)(nil)
+
+// ... and a constructor to make it ...
+func NewTracker(mynick string) *stateTracker {
+ st := &stateTracker{
+ chans: make(map[string]*channel),
+ nicks: make(map[string]*nick),
+ }
+ st.me = newNick(mynick)
+ st.nicks[mynick] = st.me
+ return st
+}
+
+// ... and a method to wipe the state clean.
+func (st *stateTracker) Wipe() {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ // Deleting all the channels implicitly deletes every nick but me.
+ for _, ch := range st.chans {
+ st.delChannel(ch)
+ }
+}
+
+/******************************************************************************\
+ * tracker methods to create/look up nicks/channels
+\******************************************************************************/
+
+// Creates a new nick, initialises it, and stores it so it
+// can be properly tracked for state management purposes.
+func (st *stateTracker) NewNick(n string) *Nick {
+ if n == "" {
+ logging.Warn("Tracker.NewNick(): Not tracking empty nick.")
+ return nil
+ }
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ if _, ok := st.nicks[n]; ok {
+ logging.Warn("Tracker.NewNick(): %s already tracked.", n)
+ return nil
+ }
+ st.nicks[n] = newNick(n)
+ return st.nicks[n].Nick()
+}
+
+// Returns a nick for the nick n, if we're tracking it.
+func (st *stateTracker) GetNick(n string) *Nick {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ if nk, ok := st.nicks[n]; ok {
+ return nk.Nick()
+ }
+ return nil
+}
+
+// Signals to the tracker that a nick should be tracked
+// under a "neu" nick rather than the old one.
+func (st *stateTracker) ReNick(old, neu string) *Nick {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ nk, ok := st.nicks[old]
+ if !ok {
+ logging.Warn("Tracker.ReNick(): %s not tracked.", old)
+ return nil
+ }
+ if _, ok := st.nicks[neu]; ok {
+ logging.Warn("Tracker.ReNick(): %s already exists.", neu)
+ return nil
+ }
+
+ nk.nick = neu
+ delete(st.nicks, old)
+ st.nicks[neu] = nk
+ for ch, _ := range nk.chans {
+ // We also need to update the lookup maps of all the channels
+ // the nick is on, to keep things in sync.
+ delete(ch.lookup, old)
+ ch.lookup[neu] = nk
+ }
+ return nk.Nick()
+}
+
+// Removes a nick from being tracked.
+func (st *stateTracker) DelNick(n string) *Nick {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ if nk, ok := st.nicks[n]; ok {
+ if nk == st.me {
+ logging.Warn("Tracker.DelNick(): won't delete myself.")
+ return nil
+ }
+ st.delNick(nk)
+ return nk.Nick()
+ }
+ logging.Warn("Tracker.DelNick(): %s not tracked.", n)
+ return nil
+}
+
+func (st *stateTracker) delNick(nk *nick) {
+ // st.mu lock held by DelNick, DelChannel or Wipe
+ if nk == st.me {
+ // Shouldn't get here => internal state tracking code is fubar.
+ logging.Error("Tracker.DelNick(): TRYING TO DELETE ME :-(")
+ return
+ }
+ delete(st.nicks, nk.nick)
+ for ch, _ := range nk.chans {
+ nk.delChannel(ch)
+ ch.delNick(nk)
+ if len(ch.nicks) == 0 {
+ // Deleting a nick from tracking shouldn't empty any channels as
+ // *we* should be on the channel with them to be tracking them.
+ logging.Error("Tracker.delNick(): deleting nick %s emptied "+
+ "channel %s, this shouldn't happen!", nk.nick, ch.name)
+ }
+ }
+}
+
+// Sets ident, host and "real" name for the nick.
+func (st *stateTracker) NickInfo(n, ident, host, name string) *Nick {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ nk, ok := st.nicks[n]
+ if !ok {
+ return nil
+ }
+ nk.ident = ident
+ nk.host = host
+ nk.name = name
+ return nk.Nick()
+}
+
+// Sets user modes for the nick.
+func (st *stateTracker) NickModes(n, modes string) *Nick {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ nk, ok := st.nicks[n]
+ if !ok {
+ return nil
+ }
+ nk.parseModes(modes)
+ return nk.Nick()
+}
+
+// Creates a new Channel, initialises it, and stores it so it
+// can be properly tracked for state management purposes.
+func (st *stateTracker) NewChannel(c string) *Channel {
+ if c == "" {
+ logging.Warn("Tracker.NewChannel(): Not tracking empty channel.")
+ return nil
+ }
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ if _, ok := st.chans[c]; ok {
+ logging.Warn("Tracker.NewChannel(): %s already tracked.", c)
+ return nil
+ }
+ st.chans[c] = newChannel(c)
+ return st.chans[c].Channel()
+}
+
+// Returns a Channel for the channel c, if we're tracking it.
+func (st *stateTracker) GetChannel(c string) *Channel {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ if ch, ok := st.chans[c]; ok {
+ return ch.Channel()
+ }
+ return nil
+}
+
+// Removes a Channel from being tracked.
+func (st *stateTracker) DelChannel(c string) *Channel {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ if ch, ok := st.chans[c]; ok {
+ st.delChannel(ch)
+ return ch.Channel()
+ }
+ logging.Warn("Tracker.DelChannel(): %s not tracked.", c)
+ return nil
+}
+
+func (st *stateTracker) delChannel(ch *channel) {
+ // st.mu lock held by DelChannel or Wipe
+ delete(st.chans, ch.name)
+ for nk, _ := range ch.nicks {
+ ch.delNick(nk)
+ nk.delChannel(ch)
+ if len(nk.chans) == 0 && nk != st.me {
+ // We're no longer in any channels with this nick.
+ st.delNick(nk)
+ }
+ }
+}
+
+// Sets the topic of a channel.
+func (st *stateTracker) Topic(c, topic string) *Channel {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ ch, ok := st.chans[c]
+ if !ok {
+ return nil
+ }
+ ch.topic = topic
+ return ch.Channel()
+}
+
+// Sets modes for a channel, including privileges like +o.
+func (st *stateTracker) ChannelModes(c, modes string, args ...string) *Channel {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ ch, ok := st.chans[c]
+ if !ok {
+ return nil
+ }
+ ch.parseModes(modes, args...)
+ return ch.Channel()
+}
+
+// Returns the Nick the state tracker thinks is Me.
+// NOTE: Nick() requires the mutex to be held.
+func (st *stateTracker) Me() *Nick {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ return st.me.Nick()
+}
+
+// Returns true if both the channel c and the nick n are tracked
+// and the nick is associated with the channel.
+func (st *stateTracker) IsOn(c, n string) (*ChanPrivs, bool) {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ nk, nok := st.nicks[n]
+ ch, cok := st.chans[c]
+ if nok && cok {
+ return nk.isOn(ch)
+ }
+ return nil, false
+}
+
+// Associates an already known nick with an already known channel.
+func (st *stateTracker) Associate(c, n string) *ChanPrivs {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ nk, nok := st.nicks[n]
+ ch, cok := st.chans[c]
+
+ if !cok {
+ // As we can implicitly delete both nicks and channels from being
+ // tracked by dissociating one from the other, we should verify that
+ // we're not being passed an old Nick or Channel.
+ logging.Error("Tracker.Associate(): channel %s not found in "+
+ "internal state.", c)
+ return nil
+ } else if !nok {
+ logging.Error("Tracker.Associate(): nick %s not found in "+
+ "internal state.", n)
+ return nil
+ } else if _, ok := nk.isOn(ch); ok {
+ logging.Warn("Tracker.Associate(): %s already on %s.",
+ nk, ch)
+ return nil
+ }
+ cp := new(ChanPrivs)
+ ch.addNick(nk, cp)
+ nk.addChannel(ch, cp)
+ return cp.Copy()
+}
+
+// Dissociates an already known nick from an already known channel.
+// Does some tidying up to stop tracking nicks we're no longer on
+// any common channels with, and channels we're no longer on.
+func (st *stateTracker) Dissociate(c, n string) {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ nk, nok := st.nicks[n]
+ ch, cok := st.chans[c]
+
+ if !cok {
+ // As we can implicitly delete both nicks and channels from being
+ // tracked by dissociating one from the other, we should verify that
+ // we're not being passed an old Nick or Channel.
+ logging.Error("Tracker.Dissociate(): channel %s not found in "+
+ "internal state.", c)
+ } else if !nok {
+ logging.Error("Tracker.Dissociate(): nick %s not found in "+
+ "internal state.", n)
+ } else if _, ok := nk.isOn(ch); !ok {
+ logging.Warn("Tracker.Dissociate(): %s not on %s.",
+ nk.nick, ch.name)
+ } else if nk == st.me {
+ // I'm leaving the channel for some reason, so it won't be tracked.
+ st.delChannel(ch)
+ } else {
+ // Remove the nick from the channel and the channel from the nick.
+ ch.delNick(nk)
+ nk.delChannel(ch)
+ if len(nk.chans) == 0 {
+ // We're no longer in any channels with this nick.
+ st.delNick(nk)
+ }
+ }
+}
+
+func (st *stateTracker) String() string {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ str := "GoIRC Channels\n"
+ str += "--------------\n\n"
+ for _, ch := range st.chans {
+ str += ch.String() + "\n"
+ }
+ str += "GoIRC NickNames\n"
+ str += "---------------\n\n"
+ for _, n := range st.nicks {
+ if n != st.me {
+ str += n.String() + "\n"
+ }
+ }
+ return str
+}
diff --git a/vendor/github.com/fluffle/goirc/state/tracker_test.go b/vendor/github.com/fluffle/goirc/state/tracker_test.go
new file mode 100644
index 0000000..1cfe8fc
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/state/tracker_test.go
@@ -0,0 +1,564 @@
+package state
+
+import (
+ "testing"
+)
+
+// There is some awkwardness in these tests. Items retrieved directly from the
+// state trackers internal maps are private and only have private,
+// uncaptialised members. Items retrieved from state tracker public interface
+// methods are public and only have public, capitalised members. Comparisons of
+// the two are done on the basis of nick or channel name.
+
+func TestSTNewTracker(t *testing.T) {
+ st := NewTracker("mynick")
+
+ if len(st.nicks) != 1 {
+ t.Errorf("Nick list of new tracker is not 1 (me!).")
+ }
+ if len(st.chans) != 0 {
+ t.Errorf("Channel list of new tracker is not empty.")
+ }
+ if nk, ok := st.nicks["mynick"]; !ok || nk.nick != "mynick" || nk != st.me {
+ t.Errorf("My nick not stored correctly in tracker.")
+ }
+}
+
+func TestSTNewNick(t *testing.T) {
+ st := NewTracker("mynick")
+ test1 := st.NewNick("test1")
+
+ if test1 == nil || test1.Nick != "test1" {
+ t.Errorf("Nick object created incorrectly by NewNick.")
+ }
+ if n, ok := st.nicks["test1"]; !ok || !test1.Equals(n.Nick()) || len(st.nicks) != 2 {
+ t.Errorf("Nick object stored incorrectly by NewNick.")
+ }
+
+ if fail := st.NewNick("test1"); fail != nil {
+ t.Errorf("Creating duplicate nick did not produce nil return.")
+ }
+ if fail := st.NewNick(""); fail != nil {
+ t.Errorf("Creating empty nick did not produce nil return.")
+ }
+}
+
+func TestSTGetNick(t *testing.T) {
+ st := NewTracker("mynick")
+ test1 := st.NewNick("test1")
+
+ if n := st.GetNick("test1"); !test1.Equals(n) {
+ t.Errorf("Incorrect nick returned by GetNick.")
+ }
+ if n := st.GetNick("test2"); n != nil {
+ t.Errorf("Nick unexpectedly returned by GetNick.")
+ }
+ if len(st.nicks) != 2 {
+ t.Errorf("Nick list changed size during GetNick.")
+ }
+}
+
+func TestSTReNick(t *testing.T) {
+ st := NewTracker("mynick")
+ test1 := st.NewNick("test1")
+
+ // This channel is here to ensure that its lookup map gets updated
+ st.NewChannel("#chan1")
+ st.Associate("#chan1", "test1")
+
+ // We need to check out the manipulation of the internals.
+ n1 := st.nicks["test1"]
+ c1 := st.chans["#chan1"]
+
+ test2 := st.ReNick("test1", "test2")
+
+ if _, ok := st.nicks["test1"]; ok {
+ t.Errorf("Nick test1 still exists after ReNick.")
+ }
+ if n, ok := st.nicks["test2"]; !ok || n != n1 {
+ t.Errorf("Nick test2 doesn't exist after ReNick.")
+ }
+ if _, ok := c1.lookup["test1"]; ok {
+ t.Errorf("Channel #chan1 still knows about test1 after ReNick.")
+ }
+ if n, ok := c1.lookup["test2"]; !ok || n != n1 {
+ t.Errorf("Channel #chan1 doesn't know about test2 after ReNick.")
+ }
+ if test1.Nick != "test1" {
+ t.Errorf("Nick test1 changed unexpectedly.")
+ }
+ if !test2.Equals(n1.Nick()) {
+ t.Errorf("Nick test2 did not change.")
+ }
+ if len(st.nicks) != 2 {
+ t.Errorf("Nick list changed size during ReNick.")
+ }
+ if len(c1.lookup) != 1 {
+ t.Errorf("Channel lookup list changed size during ReNick.")
+ }
+
+ st.NewNick("test1")
+ n2 := st.nicks["test1"]
+ fail := st.ReNick("test1", "test2")
+
+ if n, ok := st.nicks["test2"]; !ok || n != n1 {
+ t.Errorf("Nick test2 overwritten/deleted by ReNick.")
+ }
+ if n, ok := st.nicks["test1"]; !ok || n != n2 {
+ t.Errorf("Nick test1 overwritten/deleted by ReNick.")
+ }
+ if fail != nil {
+ t.Errorf("ReNick returned Nick on failure.")
+ }
+ if len(st.nicks) != 3 {
+ t.Errorf("Nick list changed size during ReNick.")
+ }
+}
+
+func TestSTDelNick(t *testing.T) {
+ st := NewTracker("mynick")
+
+ add := st.NewNick("test1")
+ del := st.DelNick("test1")
+
+ if _, ok := st.nicks["test1"]; ok {
+ t.Errorf("Nick test1 still exists after DelNick.")
+ }
+ if len(st.nicks) != 1 {
+ t.Errorf("Nick list still contains nicks after DelNick.")
+ }
+ if !add.Equals(del) {
+ t.Errorf("DelNick returned different nick.")
+ }
+
+ // Deleting unknown nick shouldn't work, but let's make sure we have a
+ // known nick first to catch any possible accidental removals.
+ st.NewNick("test1")
+ fail := st.DelNick("test2")
+ if fail != nil || len(st.nicks) != 2 {
+ t.Errorf("Deleting unknown nick had unexpected side-effects.")
+ }
+
+ // Deleting my nick shouldn't work
+ fail = st.DelNick("mynick")
+ if fail != nil || len(st.nicks) != 2 {
+ t.Errorf("Deleting myself had unexpected side-effects.")
+ }
+
+ // Test that deletion correctly dissociates nick from channels.
+ // NOTE: the two error states in delNick (as opposed to DelNick)
+ // are not tested for here, as they will only arise from programming
+ // errors in other methods.
+
+ // Create a new channel for testing purposes.
+ st.NewChannel("#test1")
+
+ // Associate both "my" nick and test1 with the channel
+ st.Associate("#test1", "mynick")
+ st.Associate("#test1", "test1")
+
+ // We need to check out the manipulation of the internals.
+ n1 := st.nicks["test1"]
+ c1 := st.chans["#test1"]
+
+ // Test we have the expected starting state (at least vaguely)
+ if len(c1.nicks) != 2 || len(st.nicks) != 2 ||
+ len(st.me.chans) != 1 || len(n1.chans) != 1 || len(st.chans) != 1 {
+ t.Errorf("Bad initial state for test DelNick() channel dissociation.")
+ }
+
+ // Actual deletion tested above...
+ st.DelNick("test1")
+
+ if len(c1.nicks) != 1 || len(st.nicks) != 1 ||
+ len(st.me.chans) != 1 || len(n1.chans) != 0 || len(st.chans) != 1 {
+ t.Errorf("Deleting nick didn't dissociate correctly from channels.")
+ }
+
+ if _, ok := c1.nicks[n1]; ok {
+ t.Errorf("Nick not removed from channel's nick map.")
+ }
+ if _, ok := c1.lookup["test1"]; ok {
+ t.Errorf("Nick not removed from channel's lookup map.")
+ }
+}
+
+func TestSTNickInfo(t *testing.T) {
+ st := NewTracker("mynick")
+ test1 := st.NewNick("test1")
+ test2 := st.NickInfo("test1", "foo", "bar", "baz")
+ test3 := st.GetNick("test1")
+
+ if test1.Equals(test2) {
+ t.Errorf("NickInfo did not return modified nick.")
+ }
+ if !test3.Equals(test2) {
+ t.Errorf("Getting nick after NickInfo returned different nick.")
+ }
+ test1.Ident, test1.Host, test1.Name = "foo", "bar", "baz"
+ if !test1.Equals(test2) {
+ t.Errorf("NickInfo did not set nick info correctly.")
+ }
+
+ if fail := st.NickInfo("test2", "foo", "bar", "baz"); fail != nil {
+ t.Errorf("NickInfo for nonexistent nick did not return nil.")
+ }
+}
+
+func TestSTNickModes(t *testing.T) {
+ st := NewTracker("mynick")
+ test1 := st.NewNick("test1")
+ test2 := st.NickModes("test1", "+iB")
+ test3 := st.GetNick("test1")
+
+ if test1.Equals(test2) {
+ t.Errorf("NickModes did not return modified nick.")
+ }
+ if !test3.Equals(test2) {
+ t.Errorf("Getting nick after NickModes returned different nick.")
+ }
+ test1.Modes.Invisible, test1.Modes.Bot = true, true
+ if !test1.Equals(test2) {
+ t.Errorf("NickModes did not set nick modes correctly.")
+ }
+
+ if fail := st.NickModes("test2", "whatevs"); fail != nil {
+ t.Errorf("NickModes for nonexistent nick did not return nil.")
+ }
+}
+
+func TestSTNewChannel(t *testing.T) {
+ st := NewTracker("mynick")
+
+ if len(st.chans) != 0 {
+ t.Errorf("Channel list of new tracker is non-zero length.")
+ }
+
+ test1 := st.NewChannel("#test1")
+
+ if test1 == nil || test1.Name != "#test1" {
+ t.Errorf("Channel object created incorrectly by NewChannel.")
+ }
+ if c, ok := st.chans["#test1"]; !ok || !test1.Equals(c.Channel()) || len(st.chans) != 1 {
+ t.Errorf("Channel object stored incorrectly by NewChannel.")
+ }
+
+ if fail := st.NewChannel("#test1"); fail != nil {
+ t.Errorf("Creating duplicate chan did not produce nil return.")
+ }
+ if fail := st.NewChannel(""); fail != nil {
+ t.Errorf("Creating empty chan did not produce nil return.")
+ }
+}
+
+func TestSTGetChannel(t *testing.T) {
+ st := NewTracker("mynick")
+
+ test1 := st.NewChannel("#test1")
+
+ if c := st.GetChannel("#test1"); !test1.Equals(c) {
+ t.Errorf("Incorrect Channel returned by GetChannel.")
+ }
+ if c := st.GetChannel("#test2"); c != nil {
+ t.Errorf("Channel unexpectedly returned by GetChannel.")
+ }
+ if len(st.chans) != 1 {
+ t.Errorf("Channel list changed size during GetChannel.")
+ }
+}
+
+func TestSTDelChannel(t *testing.T) {
+ st := NewTracker("mynick")
+
+ add := st.NewChannel("#test1")
+ del := st.DelChannel("#test1")
+
+ if _, ok := st.chans["#test1"]; ok {
+ t.Errorf("Channel test1 still exists after DelChannel.")
+ }
+ if len(st.chans) != 0 {
+ t.Errorf("Channel list still contains chans after DelChannel.")
+ }
+ if !add.Equals(del) {
+ t.Errorf("DelChannel returned different channel.")
+ }
+
+ // Deleting unknown channel shouldn't work, but let's make sure we have a
+ // known channel first to catch any possible accidental removals.
+ st.NewChannel("#test1")
+ fail := st.DelChannel("#test2")
+ if fail != nil || len(st.chans) != 1 {
+ t.Errorf("DelChannel had unexpected side-effects.")
+ }
+
+ // Test that deletion correctly dissociates channel from tracked nicks.
+ // In order to test this thoroughly we need two channels (so that delNick()
+ // is not called internally in delChannel() when len(nick1.chans) == 0.
+ st.NewChannel("#test2")
+ st.NewNick("test1")
+
+ // Associate both "my" nick and test1 with the channels
+ st.Associate("#test1", "mynick")
+ st.Associate("#test1", "test1")
+ st.Associate("#test2", "mynick")
+ st.Associate("#test2", "test1")
+
+ // We need to check out the manipulation of the internals.
+ n1 := st.nicks["test1"]
+ c1 := st.chans["#test1"]
+ c2 := st.chans["#test2"]
+
+ // Test we have the expected starting state (at least vaguely)
+ if len(c1.nicks) != 2 || len(c2.nicks) != 2 || len(st.nicks) != 2 ||
+ len(st.me.chans) != 2 || len(n1.chans) != 2 || len(st.chans) != 2 {
+ t.Errorf("Bad initial state for test DelChannel() nick dissociation.")
+ }
+
+ st.DelChannel("#test1")
+
+ // Test intermediate state. We're still on #test2 with test1, so test1
+ // shouldn't be deleted from state tracking itself just yet.
+ if len(c1.nicks) != 0 || len(c2.nicks) != 2 || len(st.nicks) != 2 ||
+ len(st.me.chans) != 1 || len(n1.chans) != 1 || len(st.chans) != 1 {
+ t.Errorf("Deleting channel didn't dissociate correctly from nicks.")
+ }
+ if _, ok := n1.chans[c1]; ok {
+ t.Errorf("Channel not removed from nick's chans map.")
+ }
+ if _, ok := n1.lookup["#test1"]; ok {
+ t.Errorf("Channel not removed from nick's lookup map.")
+ }
+
+ st.DelChannel("#test2")
+
+ // Test final state. Deleting #test2 means that we're no longer on any
+ // common channels with test1, and thus it should be removed from tracking.
+ if len(c1.nicks) != 0 || len(c2.nicks) != 0 || len(st.nicks) != 1 ||
+ len(st.me.chans) != 0 || len(n1.chans) != 0 || len(st.chans) != 0 {
+ t.Errorf("Deleting last channel didn't dissociate correctly from nicks.")
+ }
+ if _, ok := st.nicks["test1"]; ok {
+ t.Errorf("Nick not deleted correctly when on no channels.")
+ }
+ if _, ok := st.nicks["mynick"]; !ok {
+ t.Errorf("My nick deleted incorrectly when on no channels.")
+ }
+}
+
+func TestSTTopic(t *testing.T) {
+ st := NewTracker("mynick")
+ test1 := st.NewChannel("#test1")
+ test2 := st.Topic("#test1", "foo bar")
+ test3 := st.GetChannel("#test1")
+
+ if test1.Equals(test2) {
+ t.Errorf("Topic did not return modified channel.")
+ }
+ if !test3.Equals(test2) {
+ t.Errorf("Getting channel after Topic returned different channel.")
+ }
+ test1.Topic = "foo bar"
+ if !test1.Equals(test2) {
+ t.Errorf("Topic did not set channel topic correctly.")
+ }
+
+ if fail := st.Topic("#test2", "foo baz"); fail != nil {
+ t.Errorf("Topic for nonexistent channel did not return nil.")
+ }
+}
+
+func TestSTChannelModes(t *testing.T) {
+ st := NewTracker("mynick")
+ test1 := st.NewChannel("#test1")
+ test2 := st.ChannelModes("#test1", "+sk", "foo")
+ test3 := st.GetChannel("#test1")
+
+ if test1.Equals(test2) {
+ t.Errorf("ChannelModes did not return modified channel.")
+ }
+ if !test3.Equals(test2) {
+ t.Errorf("Getting channel after ChannelModes returned different channel.")
+ }
+ test1.Modes.Secret, test1.Modes.Key = true, "foo"
+ if !test1.Equals(test2) {
+ t.Errorf("ChannelModes did not set channel modes correctly.")
+ }
+
+ if fail := st.ChannelModes("test2", "whatevs"); fail != nil {
+ t.Errorf("ChannelModes for nonexistent channel did not return nil.")
+ }
+}
+
+func TestSTIsOn(t *testing.T) {
+ st := NewTracker("mynick")
+
+ st.NewNick("test1")
+ st.NewChannel("#test1")
+
+ if priv, ok := st.IsOn("#test1", "test1"); ok || priv != nil {
+ t.Errorf("test1 is not on #test1 (yet)")
+ }
+ st.Associate("#test1", "test1")
+ if priv, ok := st.IsOn("#test1", "test1"); !ok || priv == nil {
+ t.Errorf("test1 is on #test1 (now)")
+ }
+}
+
+func TestSTAssociate(t *testing.T) {
+ st := NewTracker("mynick")
+
+ st.NewNick("test1")
+ st.NewChannel("#test1")
+
+ // We need to check out the manipulation of the internals.
+ n1 := st.nicks["test1"]
+ c1 := st.chans["#test1"]
+
+ st.Associate("#test1", "test1")
+ npriv, nok := n1.chans[c1]
+ cpriv, cok := c1.nicks[n1]
+ if !nok || !cok || npriv != cpriv {
+ t.Errorf("#test1 was not associated with test1.")
+ }
+
+ // Test error cases
+ if st.Associate("", "test1") != nil {
+ t.Errorf("Associating unknown channel did not return nil.")
+ }
+ if st.Associate("#test1", "") != nil {
+ t.Errorf("Associating unknown nick did not return nil.")
+ }
+ if st.Associate("#test1", "test1") != nil {
+ t.Errorf("Associating already-associated things did not return nil.")
+ }
+}
+
+func TestSTDissociate(t *testing.T) {
+ st := NewTracker("mynick")
+
+ st.NewNick("test1")
+ st.NewChannel("#test1")
+ st.NewChannel("#test2")
+
+ // Associate both "my" nick and test1 with the channels
+ st.Associate("#test1", "mynick")
+ st.Associate("#test1", "test1")
+ st.Associate("#test2", "mynick")
+ st.Associate("#test2", "test1")
+
+ // We need to check out the manipulation of the internals.
+ n1 := st.nicks["test1"]
+ c1 := st.chans["#test1"]
+ c2 := st.chans["#test2"]
+
+ // Check the initial state looks mostly like we expect it to.
+ if len(c1.nicks) != 2 || len(c2.nicks) != 2 || len(st.nicks) != 2 ||
+ len(st.me.chans) != 2 || len(n1.chans) != 2 || len(st.chans) != 2 {
+ t.Errorf("Initial state for dissociation tests looks odd.")
+ }
+
+ // First, test the case of me leaving #test2
+ st.Dissociate("#test2", "mynick")
+
+ // This should have resulted in the complete deletion of the channel.
+ if len(c1.nicks) != 2 || len(c2.nicks) != 0 || len(st.nicks) != 2 ||
+ len(st.me.chans) != 1 || len(n1.chans) != 1 || len(st.chans) != 1 {
+ t.Errorf("Dissociating myself from channel didn't delete it correctly.")
+ }
+ if st.GetChannel("#test2") != nil {
+ t.Errorf("Able to get channel after dissociating myself.")
+ }
+
+ // Reassociating myself and test1 to #test2 shouldn't cause any errors.
+ st.NewChannel("#test2")
+ st.Associate("#test2", "mynick")
+ st.Associate("#test2", "test1")
+
+ // c2 is out of date with the complete deletion of the channel
+ c2 = st.chans["#test2"]
+
+ // Check state once moar.
+ if len(c1.nicks) != 2 || len(c2.nicks) != 2 || len(st.nicks) != 2 ||
+ len(st.me.chans) != 2 || len(n1.chans) != 2 || len(st.chans) != 2 {
+ t.Errorf("Reassociating to channel has produced unexpected state.")
+ }
+
+ // Now, lets dissociate test1 from #test1 then #test2.
+ // This first one should only result in a change in associations.
+ st.Dissociate("#test1", "test1")
+
+ if len(c1.nicks) != 1 || len(c2.nicks) != 2 || len(st.nicks) != 2 ||
+ len(st.me.chans) != 2 || len(n1.chans) != 1 || len(st.chans) != 2 {
+ t.Errorf("Dissociating a nick from one channel went wrong.")
+ }
+
+ // This second one should also delete test1
+ // as it's no longer on any common channels with us
+ st.Dissociate("#test2", "test1")
+
+ if len(c1.nicks) != 1 || len(c2.nicks) != 1 || len(st.nicks) != 1 ||
+ len(st.me.chans) != 2 || len(n1.chans) != 0 || len(st.chans) != 2 {
+ t.Errorf("Dissociating a nick from it's last channel went wrong.")
+ }
+ if st.GetNick("test1") != nil {
+ t.Errorf("Able to get nick after dissociating from all channels.")
+ }
+}
+
+func TestSTWipe(t *testing.T) {
+ st := NewTracker("mynick")
+
+ st.NewNick("test1")
+ st.NewNick("test2")
+ st.NewNick("test3")
+ st.NewChannel("#test1")
+ st.NewChannel("#test2")
+ st.NewChannel("#test3")
+
+ // Some associations
+ st.Associate("#test1", "mynick")
+ st.Associate("#test2", "mynick")
+ st.Associate("#test3", "mynick")
+
+ st.Associate("#test1", "test1")
+ st.Associate("#test2", "test2")
+ st.Associate("#test3", "test3")
+
+ st.Associate("#test1", "test2")
+ st.Associate("#test2", "test3")
+
+ st.Associate("#test1", "test3")
+
+ // We need to check out the manipulation of the internals.
+ nick1 := st.nicks["test1"]
+ nick2 := st.nicks["test2"]
+ nick3 := st.nicks["test3"]
+ chan1 := st.chans["#test1"]
+ chan2 := st.chans["#test2"]
+ chan3 := st.chans["#test3"]
+
+ // Check the state we have at this point is what we would expect.
+ if len(st.nicks) != 4 || len(st.chans) != 3 || len(st.me.chans) != 3 {
+ t.Errorf("Tracker nick/channel lists wrong length before wipe.")
+ }
+ if len(chan1.nicks) != 4 || len(chan2.nicks) != 3 || len(chan3.nicks) != 2 {
+ t.Errorf("Channel nick lists wrong length before wipe.")
+ }
+ if len(nick1.chans) != 1 || len(nick2.chans) != 2 || len(nick3.chans) != 3 {
+ t.Errorf("Nick chan lists wrong length before wipe.")
+ }
+
+ // Nuke *all* the state!
+ st.Wipe()
+
+ // Check the state we have at this point is what we would expect.
+ if len(st.nicks) != 1 || len(st.chans) != 0 || len(st.me.chans) != 0 {
+ t.Errorf("Tracker nick/channel lists wrong length after wipe.")
+ }
+ if len(chan1.nicks) != 0 || len(chan2.nicks) != 0 || len(chan3.nicks) != 0 {
+ t.Errorf("Channel nick lists wrong length after wipe.")
+ }
+ if len(nick1.chans) != 0 || len(nick2.chans) != 0 || len(nick3.chans) != 0 {
+ t.Errorf("Nick chan lists wrong length after wipe.")
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/vims b/vendor/github.com/fluffle/goirc/vims
new file mode 100644
index 0000000..8e026a5
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/vims
@@ -0,0 +1 @@
+find . -name \*.go | xargs gvim -p README.md