├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── client.go ├── client ├── commands.go ├── commands_test.go ├── connection.go ├── connection_test.go ├── dispatch.go ├── dispatch_test.go ├── doc.go ├── handlers.go ├── handlers_test.go ├── line.go ├── line_test.go ├── mocknetconn_test.go ├── sasl_test.go └── state_handlers.go ├── doc ├── rfc2811.txt ├── rfc2812.txt └── unreal32docs.html ├── go.mod ├── go.sum ├── logging ├── glog │ └── glog.go ├── golog │ └── golog.go └── logging.go ├── state ├── channel.go ├── channel_test.go ├── mock_tracker.go ├── nick.go ├── nick_test.go ├── tracker.go └── tracker_test.go └── vims /.gitignore: -------------------------------------------------------------------------------- 1 | /gobot 2 | *.[568] 3 | _obj/ 4 | _test/ 5 | *.swp 6 | *~ 7 | *.out 8 | /.gitconfig 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: go 3 | 4 | go: 5 | - 1.18 6 | - 1.17.8 7 | - 1.16.15 8 | - 1.15.15 9 | 10 | sudo : false 11 | 12 | notifications: 13 | irc: 14 | channels: 15 | - "irc.pl0rt.org#sp0rklf" 16 | skip_join: true 17 | 18 | script: 19 | - 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 20 | - go test -v ./... 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009+ Alex Bramley. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://api.travis-ci.org/fluffle/goirc.svg)](https://travis-ci.org/fluffle/goirc) 2 | 3 | GoIRC Client Framework 4 | ====================== 5 | 6 | ### Acquiring and Building 7 | 8 | Pretty simple, really: 9 | 10 | go get github.com/fluffle/goirc/client 11 | 12 | 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 ;-) 13 | 14 | See `fix/goirc.go` and the README there for a quick way to migrate from the 15 | old `go1` API. 16 | 17 | ### Using the framework 18 | 19 | Synopsis: 20 | ```go 21 | package main 22 | 23 | import ( 24 | "crypto/tls" 25 | "fmt" 26 | 27 | irc "github.com/fluffle/goirc/client" 28 | ) 29 | 30 | func main() { 31 | // Creating a simple IRC client is simple. 32 | c := irc.SimpleClient("nick") 33 | 34 | // Or, create a config and fiddle with it first: 35 | cfg := irc.NewConfig("nick") 36 | cfg.SSL = true 37 | cfg.SSLConfig = &tls.Config{ServerName: "irc.freenode.net"} 38 | cfg.Server = "irc.freenode.net:7000" 39 | cfg.NewNick = func(n string) string { return n + "^" } 40 | c = irc.Client(cfg) 41 | 42 | // Add handlers to do things here! 43 | // e.g. join a channel on connect. 44 | c.HandleFunc(irc.CONNECTED, 45 | func(conn *irc.Conn, line *irc.Line) { conn.Join("#channel") }) 46 | // And a signal on disconnect 47 | quit := make(chan bool) 48 | c.HandleFunc(irc.DISCONNECTED, 49 | func(conn *irc.Conn, line *irc.Line) { quit <- true }) 50 | 51 | // Tell client to connect. 52 | if err := c.Connect(); err != nil { 53 | fmt.Printf("Connection error: %s\n", err.Error()) 54 | } 55 | 56 | // With a "simple" client, set Server before calling Connect... 57 | c.Config().Server = "irc.freenode.net" 58 | 59 | // ... or, use ConnectTo instead. 60 | if err := c.ConnectTo("irc.freenode.net"); err != nil { 61 | fmt.Printf("Connection error: %s\n", err.Error()) 62 | } 63 | 64 | // Wait for disconnect 65 | <-quit 66 | } 67 | ``` 68 | 69 | The test client provides a good (if basic) example of how to use the framework. 70 | Reading `client/handlers.go` gives a more in-depth look at how handlers can be 71 | written. Commands to be sent to the server (e.g. PRIVMSG) are methods of the 72 | main `*Conn` struct, and can be found in `client/commands.go` (not all of the 73 | possible IRC commands are implemented yet). Events are produced directly from 74 | the messages from the IRC server, so you have to handle e.g. "332" for 75 | `RPL_TOPIC` to get the topic for a channel. 76 | 77 | The vast majority of handlers implemented within the framework deal with state 78 | tracking of all nicks in any channels that the client is also present in. These 79 | handlers are in `client/state_handlers.go`. State tracking is optional, disabled 80 | by default, and can be enabled and disabled by calling `EnableStateTracking()` 81 | and `DisableStateTracking()` respectively. Doing this while connected to an IRC 82 | server will probably result in an inconsistent state and a lot of warnings to 83 | STDERR ;-) 84 | 85 | ### Projects using GoIRC 86 | 87 | - [xdcc-cli](https://github.com/ostafen/xdcc-cli): A command line tool for searching and downloading files from the IRC network. 88 | 89 | 90 | ### Misc. 91 | 92 | Sorry the documentation is crap. Use the source, Luke. 93 | 94 | [Feedback](mailto:a.bramley@gmail.com) on design decisions is welcome. I am 95 | indebted to Matt Gruen for his work on 96 | [go-bot](http://code.google.com/p/go-bot/source/browse/irc.go) which inspired 97 | the re-organisation and channel-based communication structure of `*Conn.send()` 98 | and `*Conn.recv()`. I'm sure things could be more asynchronous, still. 99 | 100 | This code is (c) 2009-23 Alex Bramley, and released under the same licence terms 101 | as Go itself. 102 | 103 | Contributions gratefully received from: 104 | 105 | - [3onyc](https://github.com/3onyc) 106 | - [bramp](https://github.com/bramp) 107 | - [cgt](https://github.com/cgt) 108 | - [iopred](https://github.com/iopred) 109 | - [Krayons](https://github.com/Krayons) 110 | - [StalkR](https://github.com/StalkR) 111 | - [sztanpet](https://github.com/sztanpet) 112 | - [wathiede](https://github.com/wathiede) 113 | - [scrapbird](https://github.com/scrapbird) 114 | - [soul9](https://github.com/soul9) 115 | - [jakebailey](https://github.com/jakebailey) 116 | - [stapelberg](https://github.com/stapelberg) 117 | - [shammash](https://github.com/shammash) 118 | - [ostafen](https://github.com/ostafen) 119 | - [supertassu](https://github.com/supertassu) 120 | 121 | And thanks to the following for minor doc/fix PRs: 122 | 123 | - [tmcarr](https://github.com/tmcarr) 124 | - [Gentux](https://github.com/Gentux) 125 | - [kidanger](https://github.com/kidanger) 126 | - [ripcurld00d](https://github.com/ripcurld00d) 127 | - [gundalow](https://github.com/gundalow) 128 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | irc "github.com/fluffle/goirc/client" 11 | ) 12 | 13 | var host *string = flag.String("host", "irc.freenode.net", "IRC server") 14 | var channel *string = flag.String("channel", "#go-nuts", "IRC channel") 15 | 16 | func main() { 17 | flag.Parse() 18 | 19 | // create new IRC connection 20 | c := irc.SimpleClient("GoTest", "gotest") 21 | c.EnableStateTracking() 22 | c.HandleFunc("connected", 23 | func(conn *irc.Conn, line *irc.Line) { conn.Join(*channel) }) 24 | 25 | // Set up a handler to notify of disconnect events. 26 | quit := make(chan bool) 27 | c.HandleFunc("disconnected", 28 | func(conn *irc.Conn, line *irc.Line) { quit <- true }) 29 | 30 | // set up a goroutine to read commands from stdin 31 | in := make(chan string, 4) 32 | reallyquit := false 33 | go func() { 34 | con := bufio.NewReader(os.Stdin) 35 | for { 36 | s, err := con.ReadString('\n') 37 | if err != nil { 38 | // wha?, maybe ctrl-D... 39 | close(in) 40 | break 41 | } 42 | // no point in sending empty lines down the channel 43 | if len(s) > 2 { 44 | in <- s[0 : len(s)-1] 45 | } 46 | } 47 | }() 48 | 49 | // set up a goroutine to do parsey things with the stuff from stdin 50 | go func() { 51 | for cmd := range in { 52 | if cmd[0] == ':' { 53 | switch idx := strings.Index(cmd, " "); { 54 | case cmd[1] == 'd': 55 | fmt.Printf(c.String()) 56 | case cmd[1] == 'n': 57 | parts := strings.Split(cmd, " ") 58 | username := strings.TrimSpace(parts[1]) 59 | channelname := strings.TrimSpace(parts[2]) 60 | _, userIsOn := c.StateTracker().IsOn(channelname, username) 61 | fmt.Printf("Checking if %s is in %s Online: %t\n", username, channelname, userIsOn) 62 | case cmd[1] == 'f': 63 | if len(cmd) > 2 && cmd[2] == 'e' { 64 | // enable flooding 65 | c.Config().Flood = true 66 | } else if len(cmd) > 2 && cmd[2] == 'd' { 67 | // disable flooding 68 | c.Config().Flood = false 69 | } 70 | for i := 0; i < 20; i++ { 71 | c.Privmsg("#", "flood test!") 72 | } 73 | case idx == -1: 74 | continue 75 | case cmd[1] == 'q': 76 | reallyquit = true 77 | c.Quit(cmd[idx+1 : len(cmd)]) 78 | case cmd[1] == 's': 79 | reallyquit = true 80 | c.Close() 81 | case cmd[1] == 'j': 82 | c.Join(cmd[idx+1 : len(cmd)]) 83 | case cmd[1] == 'p': 84 | c.Part(cmd[idx+1 : len(cmd)]) 85 | } 86 | } else { 87 | c.Raw(cmd) 88 | } 89 | } 90 | }() 91 | 92 | for !reallyquit { 93 | // connect to server 94 | if err := c.ConnectTo(*host); err != nil { 95 | fmt.Printf("Connection error: %s\n", err) 96 | return 97 | } 98 | 99 | // wait on quit channel 100 | <-quit 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /client/commands.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | REGISTER = "REGISTER" 10 | CONNECTED = "CONNECTED" 11 | DISCONNECTED = "DISCONNECTED" 12 | ACTION = "ACTION" 13 | AUTHENTICATE = "AUTHENTICATE" 14 | AWAY = "AWAY" 15 | CAP = "CAP" 16 | CTCP = "CTCP" 17 | CTCPREPLY = "CTCPREPLY" 18 | ERROR = "ERROR" 19 | INVITE = "INVITE" 20 | JOIN = "JOIN" 21 | KICK = "KICK" 22 | MODE = "MODE" 23 | NICK = "NICK" 24 | NOTICE = "NOTICE" 25 | OPER = "OPER" 26 | PART = "PART" 27 | PASS = "PASS" 28 | PING = "PING" 29 | PONG = "PONG" 30 | PRIVMSG = "PRIVMSG" 31 | QUIT = "QUIT" 32 | TOPIC = "TOPIC" 33 | USER = "USER" 34 | VERSION = "VERSION" 35 | VHOST = "VHOST" 36 | WHO = "WHO" 37 | WHOIS = "WHOIS" 38 | defaultSplit = 450 39 | ) 40 | 41 | // cutNewLines() pares down a string to the part before the first "\r" or "\n". 42 | func cutNewLines(s string) string { 43 | r := strings.SplitN(s, "\r", 2) 44 | r = strings.SplitN(r[0], "\n", 2) 45 | return r[0] 46 | } 47 | 48 | // indexFragment looks for the last sentence split-point (defined as one of 49 | // the punctuation characters .:;,!?"' followed by a space) in the string s 50 | // and returns the index in the string after that split-point. If no split- 51 | // point is found it returns the index after the last space in s, or -1. 52 | func indexFragment(s string) int { 53 | max := -1 54 | for _, sep := range []string{". ", ": ", "; ", ", ", "! ", "? ", "\" ", "' "} { 55 | if idx := strings.LastIndex(s, sep); idx > max { 56 | max = idx 57 | } 58 | } 59 | if max > 0 { 60 | return max + 2 61 | } 62 | if idx := strings.LastIndex(s, " "); idx > 0 { 63 | return idx + 1 64 | } 65 | return -1 66 | } 67 | 68 | // splitMessage splits a message > splitLen chars at: 69 | // 1. the end of the last sentence fragment before splitLen 70 | // 2. the end of the last word before splitLen 71 | // 3. splitLen itself 72 | func splitMessage(msg string, splitLen int) (msgs []string) { 73 | // This is quite short ;-) 74 | if splitLen < 13 { 75 | splitLen = defaultSplit 76 | } 77 | for len(msg) > splitLen { 78 | idx := indexFragment(msg[:splitLen-3]) 79 | if idx < 0 { 80 | idx = splitLen - 3 81 | } 82 | msgs = append(msgs, msg[:idx]+"...") 83 | msg = msg[idx:] 84 | } 85 | return append(msgs, msg) 86 | } 87 | 88 | func splitArgs(args []string, maxLen int) []string { 89 | res := make([]string, 0) 90 | 91 | i := 0 92 | for i < len(args) { 93 | currArg := args[i] 94 | i++ 95 | 96 | for i < len(args) && len(currArg)+len(args[i])+1 < maxLen { 97 | currArg += " " + args[i] 98 | i++ 99 | } 100 | res = append(res, currArg) 101 | } 102 | return res 103 | } 104 | 105 | // Raw sends a raw line to the server, should really only be used for 106 | // debugging purposes but may well come in handy. 107 | func (conn *Conn) Raw(rawline string) { 108 | // Avoid command injection by enforcing one command per line. 109 | conn.out <- cutNewLines(rawline) 110 | } 111 | 112 | // Pass sends a PASS command to the server. 113 | // PASS password 114 | func (conn *Conn) Pass(password string) { conn.Raw(PASS + " " + password) } 115 | 116 | // Nick sends a NICK command to the server. 117 | // NICK nick 118 | func (conn *Conn) Nick(nick string) { conn.Raw(NICK + " " + nick) } 119 | 120 | // User sends a USER command to the server. 121 | // USER ident 12 * :name 122 | func (conn *Conn) User(ident, name string) { 123 | conn.Raw(USER + " " + ident + " 12 * :" + name) 124 | } 125 | 126 | // Join sends a JOIN command to the server with an optional key. 127 | // JOIN channel [key] 128 | func (conn *Conn) Join(channel string, key ...string) { 129 | k := "" 130 | if len(key) > 0 { 131 | k = " " + key[0] 132 | } 133 | conn.Raw(JOIN + " " + channel + k) 134 | } 135 | 136 | // Part sends a PART command to the server with an optional part message. 137 | // PART channel [:message] 138 | func (conn *Conn) Part(channel string, message ...string) { 139 | msg := strings.Join(message, " ") 140 | if msg != "" { 141 | msg = " :" + msg 142 | } 143 | conn.Raw(PART + " " + channel + msg) 144 | } 145 | 146 | // Kick sends a KICK command to remove a nick from a channel. 147 | // KICK channel nick [:message] 148 | func (conn *Conn) Kick(channel, nick string, message ...string) { 149 | msg := strings.Join(message, " ") 150 | if msg != "" { 151 | msg = " :" + msg 152 | } 153 | conn.Raw(KICK + " " + channel + " " + nick + msg) 154 | } 155 | 156 | // Quit sends a QUIT command to the server with an optional quit message. 157 | // QUIT [:message] 158 | func (conn *Conn) Quit(message ...string) { 159 | msg := strings.Join(message, " ") 160 | if msg == "" { 161 | msg = conn.cfg.QuitMessage 162 | } 163 | conn.Raw(QUIT + " :" + msg) 164 | } 165 | 166 | // Whois sends a WHOIS command to the server. 167 | // WHOIS nick 168 | func (conn *Conn) Whois(nick string) { conn.Raw(WHOIS + " " + nick) } 169 | 170 | // Who sends a WHO command to the server. 171 | // WHO nick 172 | func (conn *Conn) Who(nick string) { conn.Raw(WHO + " " + nick) } 173 | 174 | // Privmsg sends a PRIVMSG to the target nick or channel t. 175 | // If msg is longer than Config.SplitLen characters, multiple PRIVMSGs 176 | // will be sent to the target containing sequential parts of msg. 177 | // PRIVMSG t :msg 178 | func (conn *Conn) Privmsg(t, msg string) { 179 | prefix := PRIVMSG + " " + t + " :" 180 | for _, s := range splitMessage(msg, conn.cfg.SplitLen) { 181 | conn.Raw(prefix + s) 182 | } 183 | } 184 | 185 | // Privmsgln is the variadic version of Privmsg that formats the message 186 | // that is sent to the target nick or channel t using the 187 | // fmt.Sprintln function. 188 | // Note: Privmsgln doesn't add the '\n' character at the end of the message. 189 | func (conn *Conn) Privmsgln(t string, a ...interface{}) { 190 | msg := fmt.Sprintln(a...) 191 | // trimming the new-line character added by the fmt.Sprintln function, 192 | // since it's irrelevant. 193 | msg = msg[:len(msg)-1] 194 | conn.Privmsg(t, msg) 195 | } 196 | 197 | // Privmsgf is the variadic version of Privmsg that formats the message 198 | // that is sent to the target nick or channel t using the 199 | // fmt.Sprintf function. 200 | func (conn *Conn) Privmsgf(t, format string, a ...interface{}) { 201 | msg := fmt.Sprintf(format, a...) 202 | conn.Privmsg(t, msg) 203 | } 204 | 205 | // Notice sends a NOTICE to the target nick or channel t. 206 | // If msg is longer than Config.SplitLen characters, multiple NOTICEs 207 | // will be sent to the target containing sequential parts of msg. 208 | // NOTICE t :msg 209 | func (conn *Conn) Notice(t, msg string) { 210 | for _, s := range splitMessage(msg, conn.cfg.SplitLen) { 211 | conn.Raw(NOTICE + " " + t + " :" + s) 212 | } 213 | } 214 | 215 | // Ctcp sends a (generic) CTCP message to the target nick 216 | // or channel t, with an optional argument. 217 | // PRIVMSG t :\001CTCP arg\001 218 | func (conn *Conn) Ctcp(t, ctcp string, arg ...string) { 219 | // We need to split again here to ensure 220 | for _, s := range splitMessage(strings.Join(arg, " "), conn.cfg.SplitLen) { 221 | if s != "" { 222 | s = " " + s 223 | } 224 | // Using Raw rather than PRIVMSG here to avoid double-split problems. 225 | conn.Raw(PRIVMSG + " " + t + " :\001" + strings.ToUpper(ctcp) + s + "\001") 226 | } 227 | } 228 | 229 | // CtcpReply sends a (generic) CTCP reply to the target nick 230 | // or channel t, with an optional argument. 231 | // NOTICE t :\001CTCP arg\001 232 | func (conn *Conn) CtcpReply(t, ctcp string, arg ...string) { 233 | for _, s := range splitMessage(strings.Join(arg, " "), conn.cfg.SplitLen) { 234 | if s != "" { 235 | s = " " + s 236 | } 237 | // Using Raw rather than NOTICE here to avoid double-split problems. 238 | conn.Raw(NOTICE + " " + t + " :\001" + strings.ToUpper(ctcp) + s + "\001") 239 | } 240 | } 241 | 242 | // Version sends a CTCP "VERSION" to the target nick or channel t. 243 | func (conn *Conn) Version(t string) { conn.Ctcp(t, VERSION) } 244 | 245 | // Action sends a CTCP "ACTION" to the target nick or channel t. 246 | func (conn *Conn) Action(t, msg string) { conn.Ctcp(t, ACTION, msg) } 247 | 248 | // Topic() sends a TOPIC command for a channel. 249 | // If no topic is provided this requests that a 332 response is sent by the 250 | // server for that channel, which can then be handled to retrieve the current 251 | // channel topic. If a topic is provided the channel's topic will be set. 252 | // TOPIC channel 253 | // TOPIC channel :topic 254 | func (conn *Conn) Topic(channel string, topic ...string) { 255 | t := strings.Join(topic, " ") 256 | if t != "" { 257 | t = " :" + t 258 | } 259 | conn.Raw(TOPIC + " " + channel + t) 260 | } 261 | 262 | // Mode sends a MODE command for a target nick or channel t. 263 | // If no mode strings are provided this requests that a 324 response is sent 264 | // by the server for the target. Otherwise the mode strings are concatenated 265 | // with spaces and sent to the server. This allows e.g. 266 | // conn.Mode("#channel", "+nsk", "mykey") 267 | // 268 | // MODE t 269 | // MODE t modestring 270 | func (conn *Conn) Mode(t string, modestring ...string) { 271 | mode := strings.Join(modestring, " ") 272 | if mode != "" { 273 | mode = " " + mode 274 | } 275 | conn.Raw(MODE + " " + t + mode) 276 | } 277 | 278 | // Away sends an AWAY command to the server. 279 | // If a message is provided it sets the client's away status with that message, 280 | // otherwise it resets the client's away status. 281 | // AWAY 282 | // AWAY :message 283 | func (conn *Conn) Away(message ...string) { 284 | msg := strings.Join(message, " ") 285 | if msg != "" { 286 | msg = " :" + msg 287 | } 288 | conn.Raw(AWAY + msg) 289 | } 290 | 291 | // Invite sends an INVITE command to the server. 292 | // INVITE nick channel 293 | func (conn *Conn) Invite(nick, channel string) { 294 | conn.Raw(INVITE + " " + nick + " " + channel) 295 | } 296 | 297 | // Oper sends an OPER command to the server. 298 | // OPER user pass 299 | func (conn *Conn) Oper(user, pass string) { conn.Raw(OPER + " " + user + " " + pass) } 300 | 301 | // VHost sends a VHOST command to the server. 302 | // VHOST user pass 303 | func (conn *Conn) VHost(user, pass string) { conn.Raw(VHOST + " " + user + " " + pass) } 304 | 305 | // Ping sends a PING command to the server, which should PONG. 306 | // PING :message 307 | func (conn *Conn) Ping(message string) { conn.Raw(PING + " :" + message) } 308 | 309 | // Pong sends a PONG command to the server. 310 | // PONG :message 311 | func (conn *Conn) Pong(message string) { conn.Raw(PONG + " :" + message) } 312 | 313 | // Cap sends a CAP command to the server. 314 | // CAP subcommand 315 | // CAP subcommand :message 316 | func (conn *Conn) Cap(subcommmand string, capabilities ...string) { 317 | if len(capabilities) == 0 { 318 | conn.Raw(CAP + " " + subcommmand) 319 | } else { 320 | cmdPrefix := CAP + " " + subcommmand + " :" 321 | for _, args := range splitArgs(capabilities, defaultSplit-len(cmdPrefix)) { 322 | conn.Raw(cmdPrefix + args) 323 | } 324 | } 325 | } 326 | 327 | // Authenticate send an AUTHENTICATE command to the server. 328 | func (conn *Conn) Authenticate(message string) { 329 | conn.Raw(AUTHENTICATE + " " + message) 330 | } 331 | -------------------------------------------------------------------------------- /client/commands_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestCutNewLines(t *testing.T) { 11 | tests := []struct{ in, out string }{ 12 | {"", ""}, 13 | {"foo bar", "foo bar"}, 14 | {"foo bar\rbaz", "foo bar"}, 15 | {"foo bar\nbaz", "foo bar"}, 16 | {"blorp\r\n\r\nbloop", "blorp"}, 17 | {"\n\rblaap", ""}, 18 | {"\r\n", ""}, 19 | {"boo\\r\\n\\n\r", "boo\\r\\n\\n"}, 20 | } 21 | for i, test := range tests { 22 | out := cutNewLines(test.in) 23 | if test.out != out { 24 | t.Errorf("test %d: expected %q, got %q", i, test.out, out) 25 | } 26 | } 27 | } 28 | 29 | func TestIndexFragment(t *testing.T) { 30 | tests := []struct { 31 | in string 32 | out int 33 | }{ 34 | {"", -1}, 35 | {"foobarbaz", -1}, 36 | {"foo bar baz", 8}, 37 | {"foo. bar baz", 5}, 38 | {"foo: bar baz", 5}, 39 | {"foo; bar baz", 5}, 40 | {"foo, bar baz", 5}, 41 | {"foo! bar baz", 5}, 42 | {"foo? bar baz", 5}, 43 | {"foo\" bar baz", 5}, 44 | {"foo' bar baz", 5}, 45 | {"foo. bar. baz beep", 10}, 46 | {"foo. bar, baz beep", 10}, 47 | } 48 | for i, test := range tests { 49 | out := indexFragment(test.in) 50 | if test.out != out { 51 | t.Errorf("test %d: expected %d, got %d", i, test.out, out) 52 | } 53 | } 54 | } 55 | 56 | func TestSplitMessage(t *testing.T) { 57 | tests := []struct { 58 | in string 59 | sp int 60 | out []string 61 | }{ 62 | {"", 0, []string{""}}, 63 | {"foo", 0, []string{"foo"}}, 64 | {"foo bar baz beep", 0, []string{"foo bar baz beep"}}, 65 | {"foo bar baz beep", 15, []string{"foo bar baz ...", "beep"}}, 66 | {"foo bar, baz beep", 15, []string{"foo bar, ...", "baz beep"}}, 67 | {"0123456789012345", 0, []string{"0123456789012345"}}, 68 | {"0123456789012345", 15, []string{"012345678901...", "2345"}}, 69 | {"0123456789012345", 16, []string{"0123456789012345"}}, 70 | } 71 | for i, test := range tests { 72 | out := splitMessage(test.in, test.sp) 73 | if !reflect.DeepEqual(test.out, out) { 74 | t.Errorf("test %d: expected %q, got %q", i, test.out, out) 75 | } 76 | } 77 | } 78 | 79 | func TestClientCommands(t *testing.T) { 80 | c, s := setUp(t) 81 | defer s.tearDown() 82 | 83 | // Avoid having to type ridiculously long lines to test that 84 | // messages longer than SplitLen are correctly sent to the server. 85 | c.cfg.SplitLen = 23 86 | 87 | c.Pass("password") 88 | s.nc.Expect("PASS password") 89 | 90 | c.Nick("test") 91 | s.nc.Expect("NICK test") 92 | 93 | c.User("test", "Testing IRC") 94 | s.nc.Expect("USER test 12 * :Testing IRC") 95 | 96 | c.Raw("JUST a raw :line") 97 | s.nc.Expect("JUST a raw :line") 98 | 99 | c.Join("#foo") 100 | s.nc.Expect("JOIN #foo") 101 | c.Join("#foo bar") 102 | s.nc.Expect("JOIN #foo bar") 103 | 104 | c.Part("#foo") 105 | s.nc.Expect("PART #foo") 106 | c.Part("#foo", "Screw you guys...") 107 | s.nc.Expect("PART #foo :Screw you guys...") 108 | 109 | c.Quit() 110 | s.nc.Expect("QUIT :GoBye!") 111 | c.Quit("I'm going home.") 112 | s.nc.Expect("QUIT :I'm going home.") 113 | 114 | c.Whois("somebody") 115 | s.nc.Expect("WHOIS somebody") 116 | 117 | c.Who("*@some.host.com") 118 | s.nc.Expect("WHO *@some.host.com") 119 | 120 | c.Privmsg("#foo", "bar") 121 | s.nc.Expect("PRIVMSG #foo :bar") 122 | 123 | c.Privmsgln("#foo", "bar") 124 | s.nc.Expect("PRIVMSG #foo :bar") 125 | 126 | c.Privmsgf("#foo", "say %s", "foo") 127 | s.nc.Expect("PRIVMSG #foo :say foo") 128 | 129 | c.Privmsgln("#foo", "bar", 1, 3.54, []int{24, 36}) 130 | s.nc.Expect("PRIVMSG #foo :bar 1 3.54 [24 36]") 131 | 132 | c.Privmsgf("#foo", "user %d is at %s", 2, "home") 133 | s.nc.Expect("PRIVMSG #foo :user 2 is at home") 134 | 135 | // 0123456789012345678901234567890123 136 | c.Privmsg("#foo", "foo bar baz blorp. woo woobly woo.") 137 | s.nc.Expect("PRIVMSG #foo :foo bar baz blorp. ...") 138 | s.nc.Expect("PRIVMSG #foo :woo woobly woo.") 139 | 140 | c.Privmsgln("#foo", "foo bar baz blorp. woo woobly woo.") 141 | s.nc.Expect("PRIVMSG #foo :foo bar baz blorp. ...") 142 | s.nc.Expect("PRIVMSG #foo :woo woobly woo.") 143 | 144 | c.Privmsgf("#foo", "%s %s", "foo bar baz blorp.", "woo woobly woo.") 145 | s.nc.Expect("PRIVMSG #foo :foo bar baz blorp. ...") 146 | s.nc.Expect("PRIVMSG #foo :woo woobly woo.") 147 | 148 | c.Privmsgln("#foo", "foo bar", 3.54, "blorp.", "woo", "woobly", []int{1, 2}) 149 | s.nc.Expect("PRIVMSG #foo :foo bar 3.54 blorp. ...") 150 | s.nc.Expect("PRIVMSG #foo :woo woobly [1 2]") 151 | 152 | c.Privmsgf("#foo", "%s %.2f %s %s %s %v", "foo bar", 3.54, "blorp.", "woo", "woobly", []int{1, 2}) 153 | s.nc.Expect("PRIVMSG #foo :foo bar 3.54 blorp. ...") 154 | s.nc.Expect("PRIVMSG #foo :woo woobly [1 2]") 155 | 156 | c.Notice("somebody", "something") 157 | s.nc.Expect("NOTICE somebody :something") 158 | 159 | // 01234567890123456789012345678901234567 160 | c.Notice("somebody", "something much much longer that splits") 161 | s.nc.Expect("NOTICE somebody :something much much ...") 162 | s.nc.Expect("NOTICE somebody :longer that splits") 163 | 164 | c.Ctcp("somebody", "ping", "123456789") 165 | s.nc.Expect("PRIVMSG somebody :\001PING 123456789\001") 166 | 167 | c.Ctcp("somebody", "ping", "123456789012345678901234567890") 168 | s.nc.Expect("PRIVMSG somebody :\001PING 12345678901234567890...\001") 169 | s.nc.Expect("PRIVMSG somebody :\001PING 1234567890\001") 170 | 171 | c.CtcpReply("somebody", "pong", "123456789012345678901234567890") 172 | s.nc.Expect("NOTICE somebody :\001PONG 12345678901234567890...\001") 173 | s.nc.Expect("NOTICE somebody :\001PONG 1234567890\001") 174 | 175 | c.CtcpReply("somebody", "pong", "123456789") 176 | s.nc.Expect("NOTICE somebody :\001PONG 123456789\001") 177 | 178 | c.Version("somebody") 179 | s.nc.Expect("PRIVMSG somebody :\001VERSION\001") 180 | 181 | c.Action("#foo", "pokes somebody") 182 | s.nc.Expect("PRIVMSG #foo :\001ACTION pokes somebody\001") 183 | 184 | c.Topic("#foo") 185 | s.nc.Expect("TOPIC #foo") 186 | c.Topic("#foo", "la la la") 187 | s.nc.Expect("TOPIC #foo :la la la") 188 | 189 | c.Mode("#foo") 190 | s.nc.Expect("MODE #foo") 191 | c.Mode("#foo", "+o somebody") 192 | s.nc.Expect("MODE #foo +o somebody") 193 | 194 | c.Away() 195 | s.nc.Expect("AWAY") 196 | c.Away("Dave's not here, man.") 197 | s.nc.Expect("AWAY :Dave's not here, man.") 198 | 199 | c.Invite("somebody", "#foo") 200 | s.nc.Expect("INVITE somebody #foo") 201 | 202 | c.Oper("user", "pass") 203 | s.nc.Expect("OPER user pass") 204 | 205 | c.VHost("user", "pass") 206 | s.nc.Expect("VHOST user pass") 207 | } 208 | 209 | func TestSplitCommand(t *testing.T) { 210 | nArgs := 100 211 | 212 | args := make([]string, 0) 213 | for i := 0; i < nArgs; i++ { 214 | args = append(args, "arg"+strconv.Itoa(i)) 215 | } 216 | 217 | for maxLen := 1; maxLen <= defaultSplit; maxLen *= 2 { 218 | for _, argStr := range splitArgs(args, maxLen) { 219 | if len(argStr) > maxLen && len(strings.Split(argStr, " ")) > 1 { 220 | t.Errorf("maxLen = %d, but len(cmd) = %d", maxLen, len(argStr)) 221 | } 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /client/connection.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "crypto/tls" 7 | "fmt" 8 | "io" 9 | "net" 10 | "net/url" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | sasl "github.com/emersion/go-sasl" 16 | "github.com/fluffle/goirc/logging" 17 | "github.com/fluffle/goirc/state" 18 | "golang.org/x/net/proxy" 19 | ) 20 | 21 | // Conn encapsulates a connection to a single IRC server. Create 22 | // one with Client or SimpleClient. 23 | type Conn struct { 24 | // For preventing races on (dis)connect. 25 | mu sync.RWMutex 26 | 27 | // Contains parameters that people can tweak to change client behaviour. 28 | cfg *Config 29 | 30 | // Handlers 31 | intHandlers *hSet 32 | fgHandlers *hSet 33 | bgHandlers *hSet 34 | 35 | // State tracker for nicks and channels 36 | st state.Tracker 37 | stRemovers []Remover 38 | 39 | // I/O stuff to server 40 | dialer *net.Dialer 41 | proxyDialer proxy.Dialer 42 | sock net.Conn 43 | io *bufio.ReadWriter 44 | in chan *Line 45 | out chan string 46 | connected bool 47 | 48 | // Capabilities supported by the server 49 | supportedCaps *capSet 50 | 51 | // Capabilites currently enabled 52 | currCaps *capSet 53 | 54 | // SASL internals 55 | saslRemainingData []byte 56 | 57 | // CancelFunc and WaitGroup for goroutines 58 | die context.CancelFunc 59 | wg sync.WaitGroup 60 | 61 | // Internal counters for flood protection 62 | badness time.Duration 63 | lastsent time.Time 64 | } 65 | 66 | // Config contains options that can be passed to Client to change the 67 | // behaviour of the library during use. It is recommended that NewConfig 68 | // is used to create this struct rather than instantiating one directly. 69 | // Passing a Config with no Nick in the Me field to Client will result 70 | // in unflattering consequences. 71 | type Config struct { 72 | // Set this to provide the Nick, Ident and Name for the client to use. 73 | // It is recommended to call Conn.Me to get up-to-date information 74 | // about the current state of the client's IRC nick after connecting. 75 | Me *state.Nick 76 | 77 | // Hostname to connect to and optional connect password. 78 | // Changing these after connection will have no effect until the 79 | // client reconnects. 80 | Server, Pass string 81 | 82 | // Are we connecting via SSL? Do we care about certificate validity? 83 | // Changing these after connection will have no effect until the 84 | // client reconnects. 85 | SSL bool 86 | SSLConfig *tls.Config 87 | 88 | // To connect via proxy set the proxy url here. 89 | // Changing these after connection will have no effect until the 90 | // client reconnects. 91 | Proxy string 92 | 93 | // Local address to bind to when connecting to the server. 94 | LocalAddr string 95 | 96 | // To attempt RFC6555 parallel IPv4 and IPv6 connections if both 97 | // address families are returned for a hostname, set this to true. 98 | // Passed through to https://golang.org/pkg/net/#Dialer 99 | DualStack bool 100 | 101 | // Enable IRCv3 capability negotiation. 102 | EnableCapabilityNegotiation bool 103 | 104 | // A list of capabilities to request to the server during registration. 105 | Capabilites []string 106 | 107 | // SASL configuration to use to authenticate the connection. 108 | Sasl sasl.Client 109 | 110 | // Replaceable function to customise the 433 handler's new nick. 111 | // By default the current nick's last character is "incremented". 112 | // See DefaultNewNick implementation below for details. 113 | NewNick func(string) string 114 | 115 | // Client->server ping frequency, in seconds. Defaults to 3m. 116 | // Set to 0 to disable client-side pings. 117 | PingFreq time.Duration 118 | 119 | // The duration before a connection timeout is triggered. Defaults to 1m. 120 | // Set to 0 to wait indefinitely. 121 | Timeout time.Duration 122 | 123 | // Set this to true to disable flood protection and false to re-enable. 124 | Flood bool 125 | 126 | // Sent as the reply to a CTCP VERSION message. 127 | Version string 128 | 129 | // Sent as the default QUIT message if Quit is called with no args. 130 | QuitMessage string 131 | 132 | // Configurable panic recovery for all handlers. 133 | // Defaults to logging an error, see LogPanic. 134 | Recover func(*Conn, *Line) 135 | 136 | // Split PRIVMSGs, NOTICEs and CTCPs longer than SplitLen characters 137 | // over multiple lines. Default to 450 if not set. 138 | SplitLen int 139 | } 140 | 141 | // NewConfig creates a Config struct containing sensible defaults. 142 | // It takes one required argument: the nick to use for the client. 143 | // Subsequent string arguments set the client's ident and "real" 144 | // name, but these are optional. 145 | func NewConfig(nick string, args ...string) *Config { 146 | cfg := &Config{ 147 | Me: &state.Nick{Nick: nick}, 148 | PingFreq: 3 * time.Minute, 149 | NewNick: DefaultNewNick, 150 | Recover: (*Conn).LogPanic, // in dispatch.go 151 | SplitLen: defaultSplit, 152 | Timeout: 60 * time.Second, 153 | EnableCapabilityNegotiation: false, 154 | } 155 | cfg.Me.Ident = "goirc" 156 | if len(args) > 0 && args[0] != "" { 157 | cfg.Me.Ident = args[0] 158 | } 159 | cfg.Me.Name = "Powered by GoIRC" 160 | if len(args) > 1 && args[1] != "" { 161 | cfg.Me.Name = args[1] 162 | } 163 | cfg.Version = "Powered by GoIRC" 164 | cfg.QuitMessage = "GoBye!" 165 | return cfg 166 | } 167 | 168 | // Because networks limit nick lengths, the easy approach of appending 169 | // an '_' to a nick that is already in use can cause problems. When the 170 | // length limit is reached, the clients idea of what its nick is 171 | // ends up being different from the server. Hilarity ensues. 172 | // Thanks to github.com/purpleidea for the bug report! 173 | // Thanks to 'man ascii' for 174 | func DefaultNewNick(old string) string { 175 | if len(old) == 0 { 176 | return "_" 177 | } 178 | c := old[len(old)-1] 179 | switch { 180 | case c >= '0' && c <= '9': 181 | c = '0' + (((c - '0') + 1) % 10) 182 | case c >= 'A' && c <= '}': 183 | c = 'A' + (((c - 'A') + 1) % 61) 184 | default: 185 | c = '_' 186 | } 187 | return old[:len(old)-1] + string(c) 188 | } 189 | 190 | // SimpleClient creates a new Conn, passing its arguments to NewConfig. 191 | // If you don't need to change any client options and just want to get 192 | // started quickly, this is a convenient shortcut. 193 | func SimpleClient(nick string, args ...string) *Conn { 194 | conn := Client(NewConfig(nick, args...)) 195 | return conn 196 | } 197 | 198 | // Client takes a Config struct and returns a new Conn ready to have 199 | // handlers added and connect to a server. 200 | func Client(cfg *Config) *Conn { 201 | if cfg == nil { 202 | cfg = NewConfig("__idiot__") 203 | } 204 | if cfg.Me == nil || cfg.Me.Nick == "" || cfg.Me.Ident == "" { 205 | cfg.Me = &state.Nick{Nick: "__idiot__"} 206 | cfg.Me.Ident = "goirc" 207 | cfg.Me.Name = "Powered by GoIRC" 208 | } 209 | 210 | dialer := new(net.Dialer) 211 | dialer.Timeout = cfg.Timeout 212 | dialer.DualStack = cfg.DualStack 213 | if cfg.LocalAddr != "" { 214 | if !hasPort(cfg.LocalAddr) { 215 | cfg.LocalAddr += ":0" 216 | } 217 | 218 | local, err := net.ResolveTCPAddr("tcp", cfg.LocalAddr) 219 | if err == nil { 220 | dialer.LocalAddr = local 221 | } else { 222 | logging.Error("irc.Client(): Cannot resolve local address %s: %s", cfg.LocalAddr, err) 223 | } 224 | } 225 | 226 | if cfg.Sasl != nil && !cfg.EnableCapabilityNegotiation { 227 | logging.Warn("Enabling capability negotiation as it's required for SASL") 228 | cfg.EnableCapabilityNegotiation = true 229 | } 230 | 231 | conn := &Conn{ 232 | cfg: cfg, 233 | dialer: dialer, 234 | intHandlers: handlerSet(), 235 | fgHandlers: handlerSet(), 236 | bgHandlers: handlerSet(), 237 | stRemovers: make([]Remover, 0, len(stHandlers)), 238 | lastsent: time.Now(), 239 | supportedCaps: capabilitySet(), 240 | currCaps: capabilitySet(), 241 | saslRemainingData: nil, 242 | } 243 | conn.addIntHandlers() 244 | return conn 245 | } 246 | 247 | // Connected returns true if the client is successfully connected to 248 | // an IRC server. It becomes true when the TCP connection is established, 249 | // and false again when the connection is closed. 250 | func (conn *Conn) Connected() bool { 251 | conn.mu.RLock() 252 | defer conn.mu.RUnlock() 253 | return conn.connected 254 | } 255 | 256 | // Config returns a pointer to the Config struct used by the client. 257 | // Many of the elements of Config may be changed at any point to 258 | // affect client behaviour. To disable flood protection temporarily, 259 | // for example, a handler could do: 260 | // 261 | // conn.Config().Flood = true 262 | // // Send many lines to the IRC server, risking "excess flood" 263 | // conn.Config().Flood = false 264 | func (conn *Conn) Config() *Config { 265 | return conn.cfg 266 | } 267 | 268 | // Me returns a state.Nick that reflects the client's IRC nick at the 269 | // time it is called. If state tracking is enabled, this comes from 270 | // the tracker, otherwise it is equivalent to conn.cfg.Me. 271 | func (conn *Conn) Me() *state.Nick { 272 | if conn.st != nil { 273 | conn.cfg.Me = conn.st.Me() 274 | } 275 | return conn.cfg.Me 276 | } 277 | 278 | // StateTracker returns the state tracker being used by the client, 279 | // if tracking is enabled, and nil otherwise. 280 | func (conn *Conn) StateTracker() state.Tracker { 281 | return conn.st 282 | } 283 | 284 | // EnableStateTracking causes the client to track information about 285 | // all channels it is joined to, and all the nicks in those channels. 286 | // This can be rather handy for a number of bot-writing tasks. See 287 | // the state package for more details. 288 | // 289 | // NOTE: Calling this while connected to an IRC server may cause the 290 | // state tracker to become very confused all over STDERR if logging 291 | // is enabled. State tracking should enabled before connecting or 292 | // at a pinch while the client is not joined to any channels. 293 | func (conn *Conn) EnableStateTracking() { 294 | conn.mu.Lock() 295 | defer conn.mu.Unlock() 296 | if conn.st == nil { 297 | n := conn.cfg.Me 298 | conn.st = state.NewTracker(n.Nick) 299 | conn.st.NickInfo(n.Nick, n.Ident, n.Host, n.Name) 300 | conn.cfg.Me = conn.st.Me() 301 | conn.addSTHandlers() 302 | } 303 | } 304 | 305 | // DisableStateTracking causes the client to stop tracking information 306 | // about the channels and nicks it knows of. It will also wipe current 307 | // state from the state tracker. 308 | func (conn *Conn) DisableStateTracking() { 309 | conn.mu.Lock() 310 | defer conn.mu.Unlock() 311 | if conn.st != nil { 312 | conn.cfg.Me = conn.st.Me() 313 | conn.delSTHandlers() 314 | conn.st.Wipe() 315 | conn.st = nil 316 | } 317 | } 318 | 319 | // SupportsCapability returns true if the server supports the given capability. 320 | func (conn *Conn) SupportsCapability(cap string) bool { 321 | return conn.supportedCaps.Has(cap) 322 | } 323 | 324 | // HasCapability returns true if the given capability has been acked by the server during negotiation. 325 | func (conn *Conn) HasCapability(cap string) bool { 326 | return conn.currCaps.Has(cap) 327 | } 328 | 329 | // Per-connection state initialisation. 330 | func (conn *Conn) initialise() { 331 | conn.io = nil 332 | conn.sock = nil 333 | conn.in = make(chan *Line, 32) 334 | conn.out = make(chan string, 32) 335 | conn.die = nil 336 | if conn.st != nil { 337 | conn.st.Wipe() 338 | } 339 | } 340 | 341 | // ConnectTo connects the IRC client to "host[:port]", which should be either 342 | // a hostname or an IP address, with an optional port. It sets the client's 343 | // Config.Server to host, Config.Pass to pass if one is provided, and then 344 | // calls Connect. 345 | func (conn *Conn) ConnectTo(host string, pass ...string) error { 346 | return conn.ConnectToContext(context.Background(), host, pass...) 347 | } 348 | 349 | // ConnectToContext works like ConnectTo but uses the provided context. 350 | func (conn *Conn) ConnectToContext(ctx context.Context, host string, pass ...string) error { 351 | conn.cfg.Server = host 352 | if len(pass) > 0 { 353 | conn.cfg.Pass = pass[0] 354 | } 355 | return conn.ConnectContext(ctx) 356 | } 357 | 358 | // Connect connects the IRC client to the server configured in Config.Server. 359 | // To enable explicit SSL on the connection to the IRC server, set Config.SSL 360 | // to true before calling Connect(). The port will default to 6697 if SSL is 361 | // enabled, and 6667 otherwise. 362 | // To enable connecting via a proxy server, set Config.Proxy to the proxy URL 363 | // (example socks5://localhost:9000) before calling Connect(). 364 | // 365 | // Upon successful connection, Connected will return true and a REGISTER event 366 | // will be fired. This is mostly for internal use; it is suggested that a 367 | // handler for the CONNECTED event is used to perform any initial client work 368 | // like joining channels and sending messages. 369 | func (conn *Conn) Connect() error { 370 | return conn.ConnectContext(context.Background()) 371 | } 372 | 373 | // ConnectContext works like Connect but uses the provided context. 374 | func (conn *Conn) ConnectContext(ctx context.Context) error { 375 | // We don't want to hold conn.mu while firing the REGISTER event, 376 | // and it's much easier and less error prone to defer the unlock, 377 | // so the connect mechanics have been delegated to internalConnect. 378 | err := conn.internalConnect(ctx) 379 | if err == nil { 380 | conn.dispatch(&Line{Cmd: REGISTER, Time: time.Now()}) 381 | } 382 | return err 383 | } 384 | 385 | // internalConnect handles the work of actually connecting to the server. 386 | func (conn *Conn) internalConnect(ctx context.Context) error { 387 | conn.mu.Lock() 388 | defer conn.mu.Unlock() 389 | conn.initialise() 390 | 391 | if conn.cfg.Server == "" { 392 | return fmt.Errorf("irc.Connect(): cfg.Server must be non-empty") 393 | } 394 | if conn.connected { 395 | return fmt.Errorf("irc.Connect(): Cannot connect to %s, already connected.", conn.cfg.Server) 396 | } 397 | 398 | if !hasPort(conn.cfg.Server) { 399 | if conn.cfg.SSL { 400 | conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6697") 401 | } else { 402 | conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6667") 403 | } 404 | } 405 | 406 | if conn.cfg.Proxy != "" { 407 | s, err := conn.dialProxy(ctx) 408 | if err != nil { 409 | logging.Info("irc.Connect(): Connecting via proxy %q: %v", 410 | conn.cfg.Proxy, err) 411 | return err 412 | } 413 | conn.sock = s 414 | } else { 415 | logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server) 416 | if s, err := conn.dialer.DialContext(ctx, "tcp", conn.cfg.Server); err == nil { 417 | conn.sock = s 418 | } else { 419 | return err 420 | } 421 | } 422 | 423 | if conn.cfg.SSL { 424 | logging.Info("irc.Connect(): Performing SSL handshake.") 425 | s := tls.Client(conn.sock, conn.cfg.SSLConfig) 426 | if err := s.Handshake(); err != nil { 427 | return err 428 | } 429 | conn.sock = s 430 | } 431 | 432 | conn.postConnect(ctx, true) 433 | conn.connected = true 434 | return nil 435 | } 436 | 437 | // dialProxy handles dialling via a proxy 438 | func (conn *Conn) dialProxy(ctx context.Context) (net.Conn, error) { 439 | proxyURL, err := url.Parse(conn.cfg.Proxy) 440 | if err != nil { 441 | return nil, fmt.Errorf("parsing url: %v", err) 442 | } 443 | proxyDialer, err := proxy.FromURL(proxyURL, conn.dialer) 444 | if err != nil { 445 | return nil, fmt.Errorf("creating dialer: %v", err) 446 | } 447 | conn.proxyDialer = proxyDialer 448 | contextProxyDialer, ok := proxyDialer.(proxy.ContextDialer) 449 | if ok { 450 | logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server) 451 | return contextProxyDialer.DialContext(ctx, "tcp", conn.cfg.Server) 452 | } else { 453 | logging.Warn("Dialer for proxy does not support context, please implement DialContext") 454 | logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server) 455 | return conn.proxyDialer.Dial("tcp", conn.cfg.Server) 456 | } 457 | } 458 | 459 | // postConnect performs post-connection setup, for ease of testing. 460 | func (conn *Conn) postConnect(ctx context.Context, start bool) { 461 | conn.io = bufio.NewReadWriter( 462 | bufio.NewReader(conn.sock), 463 | bufio.NewWriter(conn.sock)) 464 | if start { 465 | ctx, conn.die = context.WithCancel(ctx) 466 | conn.wg.Add(3) 467 | go conn.send(ctx) 468 | go conn.recv() 469 | go conn.runLoop(ctx) 470 | if conn.cfg.PingFreq > 0 { 471 | conn.wg.Add(1) 472 | go conn.ping(ctx) 473 | } 474 | } 475 | } 476 | 477 | // hasPort returns true if the string hostname has a :port suffix. 478 | // It was copied from net/http for great justice. 479 | func hasPort(s string) bool { 480 | return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") 481 | } 482 | 483 | // send is started as a goroutine after a connection is established. 484 | // It shuttles data from the output channel to write(), and is killed 485 | // when the context is cancelled. 486 | func (conn *Conn) send(ctx context.Context) { 487 | for { 488 | select { 489 | case line := <-conn.out: 490 | if err := conn.write(line); err != nil { 491 | logging.Error("irc.send(): %s", err.Error()) 492 | // We can't defer this, because Close() waits for it. 493 | conn.wg.Done() 494 | conn.Close() 495 | return 496 | } 497 | case <-ctx.Done(): 498 | // control channel closed, bail out 499 | conn.wg.Done() 500 | return 501 | } 502 | } 503 | } 504 | 505 | // recv is started as a goroutine after a connection is established. 506 | // It receives "\r\n" terminated lines from the server, parses them into 507 | // Lines, and sends them to the input channel. 508 | func (conn *Conn) recv() { 509 | for { 510 | s, err := conn.io.ReadString('\n') 511 | if err != nil { 512 | if err != io.EOF { 513 | logging.Error("irc.recv(): %s", err.Error()) 514 | } 515 | // We can't defer this, because Close() waits for it. 516 | conn.wg.Done() 517 | conn.Close() 518 | return 519 | } 520 | s = strings.Trim(s, "\r\n") 521 | logging.Debug("<- %s", s) 522 | 523 | if line := ParseLine(s); line != nil { 524 | line.Time = time.Now() 525 | conn.in <- line 526 | } else { 527 | logging.Warn("irc.recv(): problems parsing line:\n %s", s) 528 | } 529 | } 530 | } 531 | 532 | // ping is started as a goroutine after a connection is established, as 533 | // long as Config.PingFreq >0. It pings the server every PingFreq seconds. 534 | func (conn *Conn) ping(ctx context.Context) { 535 | defer conn.wg.Done() 536 | tick := time.NewTicker(conn.cfg.PingFreq) 537 | for { 538 | select { 539 | case <-tick.C: 540 | conn.Ping(fmt.Sprintf("%d", time.Now().UnixNano())) 541 | case <-ctx.Done(): 542 | // control channel closed, bail out 543 | tick.Stop() 544 | return 545 | } 546 | } 547 | } 548 | 549 | // runLoop is started as a goroutine after a connection is established. 550 | // It pulls Lines from the input channel and dispatches them to any 551 | // handlers that have been registered for that IRC verb. 552 | func (conn *Conn) runLoop(ctx context.Context) { 553 | for { 554 | select { 555 | case line := <-conn.in: 556 | conn.dispatch(line) 557 | case <-ctx.Done(): 558 | // control channel closed, trigger Cancel() to clean 559 | // things up properly and bail out 560 | 561 | // We can't defer this, because Close() waits for it. 562 | conn.wg.Done() 563 | conn.Close() 564 | return 565 | } 566 | } 567 | } 568 | 569 | // write writes a \r\n terminated line of output to the connected server, 570 | // using Hybrid's algorithm to rate limit if conn.cfg.Flood is false. 571 | func (conn *Conn) write(line string) error { 572 | if !conn.cfg.Flood { 573 | if t := conn.rateLimit(len(line)); t != 0 { 574 | // sleep for the current line's time value before sending it 575 | logging.Info("irc.rateLimit(): Flood! Sleeping for %.2f secs.", 576 | t.Seconds()) 577 | <-time.After(t) 578 | } 579 | } 580 | 581 | if _, err := conn.io.WriteString(line + "\r\n"); err != nil { 582 | return err 583 | } 584 | if err := conn.io.Flush(); err != nil { 585 | return err 586 | } 587 | if strings.HasPrefix(line, "PASS") { 588 | line = "PASS **************" 589 | } 590 | logging.Debug("-> %s", line) 591 | return nil 592 | } 593 | 594 | // rateLimit implements Hybrid's flood control algorithm for outgoing lines. 595 | func (conn *Conn) rateLimit(chars int) time.Duration { 596 | // Hybrid's algorithm allows for 2 seconds per line and an additional 597 | // 1/120 of a second per character on that line. 598 | linetime := 2*time.Second + time.Duration(chars)*time.Second/120 599 | elapsed := time.Now().Sub(conn.lastsent) 600 | if conn.badness += linetime - elapsed; conn.badness < 0 { 601 | // negative badness times are badness... 602 | conn.badness = 0 603 | } 604 | conn.lastsent = time.Now() 605 | // If we've sent more than 10 second's worth of lines according to the 606 | // calculation above, then we're at risk of "Excess Flood". 607 | if conn.badness > 10*time.Second { 608 | return linetime 609 | } 610 | return 0 611 | } 612 | 613 | // Close tears down all connection-related state. It is called when either 614 | // the sending or receiving goroutines encounter an error. 615 | // It may also be used to forcibly shut down the connection to the server. 616 | func (conn *Conn) Close() error { 617 | // Guard against double-call of Close() if we get an error in send() 618 | // as calling sock.Close() will cause recv() to receive EOF in readstring() 619 | conn.mu.Lock() 620 | if !conn.connected { 621 | conn.mu.Unlock() 622 | return nil 623 | } 624 | logging.Info("irc.Close(): Disconnected from server.") 625 | conn.connected = false 626 | err := conn.sock.Close() 627 | if conn.die != nil { 628 | conn.die() 629 | } 630 | // Drain both in and out channels to avoid a deadlock if the buffers 631 | // have filled. See TestSendDeadlockOnFullBuffer in connection_test.go. 632 | conn.drainIn() 633 | conn.drainOut() 634 | conn.wg.Wait() 635 | conn.mu.Unlock() 636 | // Dispatch after closing connection but before reinit 637 | // so event handlers can still access state information. 638 | conn.dispatch(&Line{Cmd: DISCONNECTED, Time: time.Now()}) 639 | return err 640 | } 641 | 642 | // drainIn sends all data buffered in conn.in to /dev/null. 643 | func (conn *Conn) drainIn() { 644 | for { 645 | select { 646 | case <-conn.in: 647 | default: 648 | return 649 | } 650 | } 651 | } 652 | 653 | // drainOut does the same for conn.out. Generics! 654 | func (conn *Conn) drainOut() { 655 | for { 656 | select { 657 | case <-conn.out: 658 | default: 659 | return 660 | } 661 | } 662 | } 663 | 664 | // Dumps a load of information about the current state of the connection to a 665 | // string for debugging state tracking and other such things. 666 | func (conn *Conn) String() string { 667 | str := "GoIRC Connection\n" 668 | str += "----------------\n\n" 669 | if conn.Connected() { 670 | str += "Connected to " + conn.cfg.Server + "\n\n" 671 | } else { 672 | str += "Not currently connected!\n\n" 673 | } 674 | str += conn.Me().String() + "\n" 675 | if conn.st != nil { 676 | str += conn.st.String() + "\n" 677 | } 678 | return str 679 | } 680 | -------------------------------------------------------------------------------- /client/connection_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "runtime" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/fluffle/goirc/state" 11 | "github.com/golang/mock/gomock" 12 | ) 13 | 14 | type checker struct { 15 | t *testing.T 16 | c chan struct{} 17 | } 18 | 19 | func callCheck(t *testing.T) checker { 20 | return checker{t: t, c: make(chan struct{})} 21 | } 22 | 23 | func (c checker) call() { 24 | c.c <- struct{}{} 25 | } 26 | 27 | func (c checker) Handle(_ *Conn, _ *Line) { 28 | c.call() 29 | } 30 | 31 | func (c checker) assertNotCalled(fmt string, args ...interface{}) { 32 | select { 33 | case <-c.c: 34 | c.t.Errorf(fmt, args...) 35 | default: 36 | } 37 | } 38 | 39 | func (c checker) assertWasCalled(fmt string, args ...interface{}) { 40 | select { 41 | case <-c.c: 42 | case <-time.After(time.Millisecond): 43 | // Usually need to wait for goroutines to settle :-/ 44 | c.t.Errorf(fmt, args...) 45 | } 46 | } 47 | 48 | type testState struct { 49 | ctrl *gomock.Controller 50 | st *state.MockTracker 51 | nc *mockNetConn 52 | c *Conn 53 | } 54 | 55 | // NOTE: including a second argument at all prevents calling c.postConnect() 56 | func setUp(t *testing.T, start ...bool) (*Conn, *testState) { 57 | ctrl := gomock.NewController(t) 58 | st := state.NewMockTracker(ctrl) 59 | nc := MockNetConn(t) 60 | c := SimpleClient("test", "test", "Testing IRC") 61 | c.initialise() 62 | ctx := context.Background() 63 | 64 | c.st = st 65 | c.sock = nc 66 | c.cfg.Flood = true // Tests can take a while otherwise 67 | c.connected = true 68 | // If a second argument is passed to setUp, we tell postConnect not to 69 | // start the various goroutines that shuttle data around. 70 | c.postConnect(ctx, len(start) == 0) 71 | // Sleep 1ms to allow background routines to start. 72 | <-time.After(time.Millisecond) 73 | 74 | return c, &testState{ctrl, st, nc, c} 75 | } 76 | 77 | func (s *testState) tearDown() { 78 | s.nc.ExpectNothing() 79 | s.c.Close() 80 | s.ctrl.Finish() 81 | } 82 | 83 | // Practically the same as the above test, but Close is called implicitly 84 | // by recv() getting an EOF from the mock connection. 85 | func TestEOF(t *testing.T) { 86 | c, s := setUp(t) 87 | // Since we're not using tearDown() here, manually call Finish() 88 | defer s.ctrl.Finish() 89 | 90 | // Set up a handler to detect whether disconnected handlers are called 91 | dcon := callCheck(t) 92 | c.Handle(DISCONNECTED, dcon) 93 | 94 | // Simulate EOF from server 95 | s.nc.Close() 96 | 97 | // Verify that disconnected handler was called 98 | dcon.assertWasCalled("Conn did not call disconnected handlers.") 99 | 100 | // Verify that the connection no longer thinks it's connected 101 | if c.Connected() { 102 | t.Errorf("Conn still thinks it's connected to the server.") 103 | } 104 | } 105 | 106 | func TestCleanupOnContextDone(t *testing.T) { 107 | c, s := setUp(t) 108 | // Since we're not using tearDown() here, manually call Finish() 109 | defer s.ctrl.Finish() 110 | 111 | // Close() triggers DISCONNECT handler after cleaning up the state 112 | // use this as a proxy to check that Close() was indeed called 113 | dcon := callCheck(t) 114 | c.Handle(DISCONNECTED, dcon) 115 | 116 | // Simulate context cancelation using our cancel func 117 | c.die() 118 | 119 | // Verify that disconnected handler was called 120 | dcon.assertWasCalled("Conn did not call disconnected handlers.") 121 | 122 | // Verify that the connection no longer thinks it's connected 123 | if c.Connected() { 124 | t.Errorf("Conn still thinks it's connected to the server.") 125 | } 126 | } 127 | 128 | func TestClientAndStateTracking(t *testing.T) { 129 | ctrl := gomock.NewController(t) 130 | st := state.NewMockTracker(ctrl) 131 | c := SimpleClient("test", "test", "Testing IRC") 132 | 133 | // Assert some basic things about the initial state of the Conn struct 134 | me := c.cfg.Me 135 | if me.Nick != "test" || me.Ident != "test" || 136 | me.Name != "Testing IRC" || me.Host != "" { 137 | t.Errorf("Conn.cfg.Me not correctly initialised.") 138 | } 139 | // Check that the internal handlers are correctly set up 140 | for k, _ := range intHandlers { 141 | if _, ok := c.intHandlers.set[strings.ToLower(k)]; !ok { 142 | t.Errorf("Missing internal handler for '%s'.", k) 143 | } 144 | } 145 | 146 | // Now enable the state tracking code and check its handlers 147 | c.EnableStateTracking() 148 | for k, _ := range stHandlers { 149 | if _, ok := c.intHandlers.set[strings.ToLower(k)]; !ok { 150 | t.Errorf("Missing state handler for '%s'.", k) 151 | } 152 | } 153 | if len(c.stRemovers) != len(stHandlers) { 154 | t.Errorf("Incorrect number of Removers (%d != %d) when adding state handlers.", 155 | len(c.stRemovers), len(stHandlers)) 156 | } 157 | if neu := c.Me(); neu.Nick != me.Nick || neu.Ident != me.Ident || 158 | neu.Name != me.Name || neu.Host != me.Host { 159 | t.Errorf("Enabling state tracking erased information about me!") 160 | } 161 | 162 | // We're expecting the untracked me to be replaced by a tracked one 163 | if c.st == nil { 164 | t.Errorf("State tracker not enabled correctly.") 165 | } 166 | if me = c.cfg.Me; me.Nick != "test" || me.Ident != "test" || 167 | me.Name != "Testing IRC" || me.Host != "" { 168 | t.Errorf("Enabling state tracking did not replace Me correctly.") 169 | } 170 | 171 | // Now, shim in the mock state tracker and test disabling state tracking 172 | c.st = st 173 | gomock.InOrder( 174 | st.EXPECT().Me().Return(me), 175 | st.EXPECT().Wipe(), 176 | ) 177 | c.DisableStateTracking() 178 | if c.st != nil || !c.cfg.Me.Equals(me) { 179 | t.Errorf("State tracker not disabled correctly.") 180 | } 181 | 182 | // Finally, check state tracking handlers were all removed correctly 183 | for k, _ := range stHandlers { 184 | if _, ok := c.intHandlers.set[strings.ToLower(k)]; ok && k != "NICK" { 185 | // A bit leaky, because intHandlers adds a NICK handler. 186 | t.Errorf("State handler for '%s' not removed correctly.", k) 187 | } 188 | } 189 | if len(c.stRemovers) != 0 { 190 | t.Errorf("stRemovers not zeroed correctly when removing state handlers.") 191 | } 192 | ctrl.Finish() 193 | } 194 | 195 | func TestSendExitsOnCancel(t *testing.T) { 196 | // Passing a second value to setUp stops goroutines from starting 197 | c, s := setUp(t, false) 198 | defer s.tearDown() 199 | 200 | // Assert that before send is running, nothing should be sent to the socket 201 | // but writes to the buffered channel "out" should not block. 202 | c.out <- "SENT BEFORE START" 203 | s.nc.ExpectNothing() 204 | 205 | // We want to test that the a goroutine calling send will exit correctly. 206 | exited := callCheck(t) 207 | ctx, cancel := context.WithCancel(context.Background()) 208 | // send() will decrement the WaitGroup, so we must increment it. 209 | c.wg.Add(1) 210 | go func() { 211 | c.send(ctx) 212 | exited.call() 213 | }() 214 | 215 | // send is now running in the background as if started by postConnect. 216 | // This should read the line previously buffered in c.out, and write it 217 | // to the socket connection. 218 | s.nc.Expect("SENT BEFORE START") 219 | 220 | // Send another line, just to be sure :-) 221 | c.out <- "SENT AFTER START" 222 | s.nc.Expect("SENT AFTER START") 223 | 224 | // Now, cancel the context to exit send and kill the goroutine. 225 | exited.assertNotCalled("Exited before signal sent.") 226 | cancel() 227 | exited.assertWasCalled("Didn't exit after signal.") 228 | s.nc.ExpectNothing() 229 | 230 | // Sending more on c.out shouldn't reach the network. 231 | c.out <- "SENT AFTER END" 232 | s.nc.ExpectNothing() 233 | } 234 | 235 | func TestSendExitsOnWriteError(t *testing.T) { 236 | // Passing a second value to setUp stops goroutines from starting 237 | c, s := setUp(t, false) 238 | // We can't use tearDown here because we're testing shutdown conditions 239 | // (and so need to EXPECT() a call to st.Wipe() in the right place) 240 | defer s.ctrl.Finish() 241 | 242 | // We want to test that the a goroutine calling send will exit correctly. 243 | exited := callCheck(t) 244 | // send() will decrement the WaitGroup, so we must increment it. 245 | c.wg.Add(1) 246 | go func() { 247 | c.send(context.Background()) 248 | exited.call() 249 | }() 250 | 251 | // Send a line to be sure things are good. 252 | c.out <- "SENT AFTER START" 253 | s.nc.Expect("SENT AFTER START") 254 | 255 | // Now, close the underlying socket to cause write() to return an error. 256 | // This will call Close() => a call to st.Wipe() will happen. 257 | exited.assertNotCalled("Exited before signal sent.") 258 | s.nc.Close() 259 | // Sending more on c.out shouldn't reach the network, but we need to send 260 | // *something* to trigger a call to write() that will fail. 261 | c.out <- "SENT AFTER END" 262 | exited.assertWasCalled("Didn't exit after signal.") 263 | s.nc.ExpectNothing() 264 | } 265 | 266 | func TestSendDeadlockOnFullBuffer(t *testing.T) { 267 | // Passing a second value to setUp stops goroutines from starting 268 | c, s := setUp(t, false) 269 | // We can't use tearDown here because we're testing a deadlock condition 270 | // and if tearDown tries to call Close() it will deadlock some more 271 | // because send() is holding the conn mutex via Close() already. 272 | defer s.ctrl.Finish() 273 | 274 | // We want to test that the a goroutine calling send will exit correctly. 275 | loopExit := callCheck(t) 276 | sendExit := callCheck(t) 277 | ctx, cancel := context.WithCancel(context.Background()) 278 | // send() and runLoop() will decrement the WaitGroup, so we must increment it. 279 | c.wg.Add(2) 280 | 281 | // The deadlock arises when a handler being called from conn.dispatch() in 282 | // runLoop() tries to write to conn.out to send a message back to the IRC 283 | // server, but the buffer is full. If at the same time send() is 284 | // calling conn.Close() and waiting in there for runLoop() to call 285 | // conn.wg.Done(), it will not empty the buffer of conn.out => deadlock. 286 | // 287 | // We simulate this by artifically filling conn.out. We must use a 288 | // goroutine to put in one more line than the buffer can hold, because 289 | // send() will read a line from conn.out on its first loop iteration: 290 | go func() { 291 | for i := 0; i < 33; i++ { 292 | c.out <- "FILL BUFFER WITH CRAP" 293 | } 294 | }() 295 | // Then we add a handler that tries to write a line to conn.out: 296 | c.HandleFunc(PRIVMSG, func(conn *Conn, line *Line) { 297 | conn.Raw(line.Raw) 298 | }) 299 | // And trigger it by starting runLoop and inserting a line into conn.in: 300 | go func() { 301 | c.runLoop(ctx) 302 | loopExit.call() 303 | }() 304 | c.in <- &Line{Cmd: PRIVMSG, Raw: "WRITE THAT CAUSES DEADLOCK"} 305 | 306 | // At this point the handler should be blocked on a write to conn.out, 307 | // preventing runLoop from looping and thus noticng the cancelled context. 308 | // 309 | // The next part is to force send() to call conn.Close(), which can 310 | // be done by closing the fake net.Conn so that it returns an error on 311 | // calls to Write(): 312 | s.nc.ExpectNothing() 313 | s.nc.Close() 314 | 315 | // Now when send is started it will read one line from conn.out and try 316 | // to write it to the socket. It should immediately receive an error and 317 | // call conn.Close(), triggering the deadlock as it waits forever for 318 | // runLoop to call conn.wg.Done. 319 | c.die = cancel // Close needs to cancel the context for us. 320 | go func() { 321 | c.send(ctx) 322 | sendExit.call() 323 | }() 324 | 325 | // Make sure that things are definitely deadlocked. 326 | <-time.After(time.Millisecond) 327 | 328 | // Verify that the connection no longer thinks it's connected, i.e. 329 | // conn.Close() has definitely been called. We can't call 330 | // conn.Connected() here because conn.Close() holds the mutex. 331 | if c.connected { 332 | t.Errorf("Conn still thinks it's connected to the server.") 333 | } 334 | 335 | // We expect both loops to terminate cleanly. If either of them don't 336 | // then we have successfully deadlocked :-( 337 | loopExit.assertWasCalled("runLoop did not exit cleanly.") 338 | sendExit.assertWasCalled("send did not exit cleanly.") 339 | } 340 | 341 | func TestRecv(t *testing.T) { 342 | // Passing a second value to setUp stops goroutines from starting 343 | c, s := setUp(t, false) 344 | // We can't use tearDown here because we're testing shutdown conditions 345 | // (and so need to EXPECT() a call to st.Wipe() in the right place) 346 | defer s.ctrl.Finish() 347 | 348 | // Send a line before recv is started up, to verify nothing appears on c.in 349 | s.nc.Send(":irc.server.org 001 test :First test line.") 350 | 351 | // reader is a helper to do a "non-blocking" read of c.in 352 | reader := func() *Line { 353 | select { 354 | case <-time.After(time.Millisecond): 355 | case l := <-c.in: 356 | return l 357 | } 358 | return nil 359 | } 360 | if l := reader(); l != nil { 361 | t.Errorf("Line parsed before recv started.") 362 | } 363 | 364 | // We want to test that the a goroutine calling recv will exit correctly. 365 | exited := callCheck(t) 366 | // recv() will decrement the WaitGroup, so we must increment it. 367 | c.wg.Add(1) 368 | go func() { 369 | c.recv() 370 | exited.call() 371 | }() 372 | 373 | // Now, this should mean that we'll receive our parsed line on c.in 374 | if l := reader(); l == nil || l.Cmd != "001" { 375 | t.Errorf("Bad first line received on input channel") 376 | } 377 | 378 | // Send a second line, just to be sure. 379 | s.nc.Send(":irc.server.org 002 test :Second test line.") 380 | if l := reader(); l == nil || l.Cmd != "002" { 381 | t.Errorf("Bad second line received on input channel.") 382 | } 383 | 384 | // Test that recv does something useful with a line it can't parse 385 | // (not that there are many, ParseLine is forgiving). 386 | s.nc.Send(":textwithnospaces") 387 | if l := reader(); l != nil { 388 | t.Errorf("Bad line still caused receive on input channel.") 389 | } 390 | 391 | // The only way recv() exits is when the socket closes. 392 | exited.assertNotCalled("Exited before socket close.") 393 | s.nc.Close() 394 | exited.assertWasCalled("Didn't exit on socket close.") 395 | 396 | // Since s.nc is closed we can't attempt another send on it... 397 | if l := reader(); l != nil { 398 | t.Errorf("Line received on input channel after socket close.") 399 | } 400 | } 401 | 402 | func TestPing(t *testing.T) { 403 | // Passing a second value to setUp stops goroutines from starting 404 | c, s := setUp(t, false) 405 | defer s.tearDown() 406 | 407 | res := time.Millisecond 408 | 409 | // Windows has a timer resolution of 15.625ms by default. 410 | // This means the test will be slower on windows, but 411 | // should at least stop most of the flakiness... 412 | // https://github.com/fluffle/goirc/issues/88 413 | if runtime.GOOS == "windows" { 414 | res = 15625 * time.Microsecond 415 | } 416 | 417 | // Set a low ping frequency for testing. 418 | c.cfg.PingFreq = 10 * res 419 | 420 | // reader is a helper to do a "non-blocking" read of c.out 421 | reader := func() string { 422 | select { 423 | case <-time.After(res): 424 | case s := <-c.out: 425 | return s 426 | } 427 | return "" 428 | } 429 | if s := reader(); s != "" { 430 | t.Errorf("Line output before ping started.") 431 | } 432 | 433 | // Start ping loop. 434 | exited := callCheck(t) 435 | ctx, cancel := context.WithCancel(context.Background()) 436 | // ping() will decrement the WaitGroup, so we must increment it. 437 | c.wg.Add(1) 438 | go func() { 439 | c.ping(ctx) 440 | exited.call() 441 | }() 442 | 443 | // The first ping should be after 10*res ms, 444 | // so we don't expect anything now on c.in 445 | if s := reader(); s != "" { 446 | t.Errorf("Line output directly after ping started.") 447 | } 448 | 449 | <-time.After(c.cfg.PingFreq) 450 | if s := reader(); s == "" || !strings.HasPrefix(s, "PING :") { 451 | t.Errorf("Line not output after %s.", c.cfg.PingFreq) 452 | } 453 | 454 | // Reader waits for res ms and we call it a few times above. 455 | <-time.After(7 * res) 456 | if s := reader(); s != "" { 457 | t.Errorf("Line output <%s after last ping.", 7*res) 458 | } 459 | 460 | // This is a short window in which the ping should happen 461 | // This may result in flaky tests; sorry (and file a bug) if so. 462 | <-time.After(2 * res) 463 | if s := reader(); s == "" || !strings.HasPrefix(s, "PING :") { 464 | t.Errorf("Line not output after another %s.", 2*res) 465 | } 466 | 467 | // Now kill the ping loop by cancelling the context. 468 | exited.assertNotCalled("Exited before signal sent.") 469 | cancel() 470 | exited.assertWasCalled("Didn't exit after signal.") 471 | // Make sure we're no longer pinging by waiting >2x PingFreq 472 | <-time.After(2*c.cfg.PingFreq + res) 473 | if s := reader(); s != "" { 474 | t.Errorf("Line output after ping stopped.") 475 | } 476 | } 477 | 478 | func TestRunLoop(t *testing.T) { 479 | // Passing a second value to setUp stops goroutines from starting 480 | c, s := setUp(t, false) 481 | defer s.tearDown() 482 | 483 | // Set up a handler to detect whether 002 handler is called. 484 | // Don't use 001 here, since there's already a handler for that 485 | // and it hangs this test unless we mock the state tracker calls. 486 | h002 := callCheck(t) 487 | c.Handle("002", h002) 488 | h003 := callCheck(t) 489 | // Set up a handler to detect whether 002 handler is called 490 | c.Handle("003", h003) 491 | 492 | l2 := ParseLine(":irc.server.org 002 test :First test line.") 493 | c.in <- l2 494 | h002.assertNotCalled("002 handler called before runLoop started.") 495 | 496 | // We want to test that the a goroutine calling runLoop will exit correctly. 497 | // Now, we can expect the call to Dispatch to take place as runLoop starts. 498 | exited := callCheck(t) 499 | ctx, cancel := context.WithCancel(context.Background()) 500 | // runLoop() will decrement the WaitGroup, so we must increment it. 501 | c.wg.Add(1) 502 | go func() { 503 | c.runLoop(ctx) 504 | exited.call() 505 | }() 506 | h002.assertWasCalled("002 handler not called after runLoop started.") 507 | 508 | // Send another line, just to be sure :-) 509 | h003.assertNotCalled("003 handler called before expected.") 510 | l3 := ParseLine(":irc.server.org 003 test :Second test line.") 511 | c.in <- l3 512 | h003.assertWasCalled("003 handler not called while runLoop started.") 513 | 514 | // Now, cancel the context to exit runLoop and kill the goroutine. 515 | exited.assertNotCalled("Exited before signal sent.") 516 | cancel() 517 | exited.assertWasCalled("Didn't exit after signal.") 518 | 519 | // Sending more on c.in shouldn't dispatch any further events 520 | c.in <- l2 521 | h002.assertNotCalled("002 handler called after runLoop ended.") 522 | } 523 | 524 | func TestWrite(t *testing.T) { 525 | // Passing a second value to setUp stops goroutines from starting 526 | c, s := setUp(t, false) 527 | // We can't use tearDown here because we're testing shutdown conditions 528 | // (and so need to EXPECT() a call to st.Wipe() in the right place) 529 | defer s.ctrl.Finish() 530 | 531 | // Write should just write a line to the socket. 532 | if err := c.write("yo momma"); err != nil { 533 | t.Errorf("Write returned unexpected error %v", err) 534 | } 535 | s.nc.Expect("yo momma") 536 | 537 | // Flood control is disabled -- setUp sets c.cfg.Flood = true -- so we should 538 | // not have set c.badness at this point. 539 | if c.badness != 0 { 540 | t.Errorf("Flood control used when Flood = true.") 541 | } 542 | 543 | c.cfg.Flood = false 544 | if err := c.write("she so useless"); err != nil { 545 | t.Errorf("Write returned unexpected error %v", err) 546 | } 547 | s.nc.Expect("she so useless") 548 | 549 | // The lastsent time should have been updated very recently... 550 | if time.Now().Sub(c.lastsent) > time.Millisecond { 551 | t.Errorf("Flood control not used when Flood = false.") 552 | } 553 | 554 | // Finally, test the error state by closing the socket then writing. 555 | s.nc.Close() 556 | if err := c.write("she can't pass unit tests"); err == nil { 557 | t.Errorf("Expected write to return error after socket close.") 558 | } 559 | } 560 | 561 | func TestRateLimit(t *testing.T) { 562 | c, s := setUp(t) 563 | defer s.tearDown() 564 | 565 | if c.badness != 0 { 566 | t.Errorf("Bad initial values for rate limit variables.") 567 | } 568 | 569 | // We'll be needing this later... 570 | abs := func(i time.Duration) time.Duration { 571 | if i < 0 { 572 | return -i 573 | } 574 | return i 575 | } 576 | 577 | // Since the changes to the time module, c.lastsent is now a time.Time. 578 | // It's initialised on client creation to time.Now() which for the purposes 579 | // of this test was probably around 1.2 ms ago. This is inconvenient. 580 | // Making it >10s ago effectively clears out the inconsistency, as this 581 | // makes elapsed > linetime and thus zeros c.badness and resets c.lastsent. 582 | c.lastsent = time.Now().Add(-10 * time.Second) 583 | if l := c.rateLimit(60); l != 0 || c.badness != 0 { 584 | t.Errorf("Rate limit got non-zero badness from long-ago lastsent.") 585 | } 586 | 587 | // So, time at the nanosecond resolution is a bit of a bitch. Choosing 60 588 | // characters as the line length means we should be increasing badness by 589 | // 2.5 seconds minus the delta between the two ratelimit calls. This should 590 | // be minimal but it's guaranteed that it won't be zero. Use 20us as a fuzz. 591 | if l := c.rateLimit(60); l != 0 || 592 | abs(c.badness-2500*time.Millisecond) > 20*time.Microsecond { 593 | t.Errorf("Rate limit calculating badness incorrectly.") 594 | } 595 | // At this point, we can tip over the badness scale, with a bit of help. 596 | // 720 chars => +8 seconds of badness => 10.5 seconds => ratelimit 597 | if l := c.rateLimit(720); l != 8*time.Second || 598 | abs(c.badness-10500*time.Millisecond) > 20*time.Microsecond { 599 | t.Errorf("Rate limit failed to return correct limiting values.") 600 | t.Errorf("l=%d, badness=%d", l, c.badness) 601 | } 602 | } 603 | 604 | func TestDefaultNewNick(t *testing.T) { 605 | tests := []struct{ in, want string }{ 606 | {"", "_"}, 607 | {"0", "1"}, 608 | {"9", "0"}, 609 | {"A", "B"}, 610 | {"Z", "["}, 611 | {"_", "`"}, 612 | {"`", "a"}, 613 | {"}", "A"}, 614 | {"-", "_"}, 615 | {"fluffle", "flufflf"}, 616 | } 617 | 618 | for _, test := range tests { 619 | if got := DefaultNewNick(test.in); got != test.want { 620 | t.Errorf("DefaultNewNick(%q) = %q, want %q", test.in, got, test.want) 621 | } 622 | } 623 | } 624 | -------------------------------------------------------------------------------- /client/dispatch.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "runtime" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/fluffle/goirc/logging" 9 | ) 10 | 11 | // Handlers are triggered on incoming Lines from the server, with the handler 12 | // "name" being equivalent to Line.Cmd. Read the RFCs for details on what 13 | // replies could come from the server. They'll generally be things like 14 | // "PRIVMSG", "JOIN", etc. but all the numeric replies are left as ascii 15 | // strings of digits like "332" (mainly because I really didn't feel like 16 | // putting massive constant tables in). 17 | // 18 | // Foreground handlers have a guarantee of protocol consistency: all the 19 | // handlers for one event will have finished before the handlers for the 20 | // next start processing. They are run in parallel but block the event 21 | // loop, so care should be taken to ensure these handlers are quick :-) 22 | // 23 | // Background handlers are run in parallel and do not block the event loop. 24 | // This is useful for things that may need to do significant work. 25 | type Handler interface { 26 | Handle(*Conn, *Line) 27 | } 28 | 29 | // Removers allow for a handler that has been previously added to the client 30 | // to be removed. 31 | type Remover interface { 32 | Remove() 33 | } 34 | 35 | // HandlerFunc allows a bare function with this signature to implement the 36 | // Handler interface. It is used by Conn.HandleFunc. 37 | type HandlerFunc func(*Conn, *Line) 38 | 39 | func (hf HandlerFunc) Handle(conn *Conn, line *Line) { 40 | hf(conn, line) 41 | } 42 | 43 | // Handlers are organised using a map of linked-lists, with each map 44 | // key representing an IRC verb or numeric, and the linked list values 45 | // being handlers that are executed in parallel when a Line from the 46 | // server with that verb or numeric arrives. 47 | type hSet struct { 48 | set map[string]*hList 49 | sync.RWMutex 50 | } 51 | 52 | type hList struct { 53 | start, end *hNode 54 | } 55 | 56 | // Storing the forward and backward links in the node allows O(1) removal. 57 | // This probably isn't strictly necessary but I think it's kinda nice. 58 | type hNode struct { 59 | next, prev *hNode 60 | set *hSet 61 | event string 62 | handler Handler 63 | } 64 | 65 | // A hNode implements both Handler (with configurable panic recovery)... 66 | func (hn *hNode) Handle(conn *Conn, line *Line) { 67 | defer conn.cfg.Recover(conn, line) 68 | hn.handler.Handle(conn, line) 69 | } 70 | 71 | // ... and Remover. 72 | func (hn *hNode) Remove() { 73 | hn.set.remove(hn) 74 | } 75 | 76 | func handlerSet() *hSet { 77 | return &hSet{set: make(map[string]*hList)} 78 | } 79 | 80 | // When a new Handler is added for an event, it is wrapped in a hNode and 81 | // returned as a Remover so the caller can remove it at a later time. 82 | func (hs *hSet) add(ev string, h Handler) Remover { 83 | hs.Lock() 84 | defer hs.Unlock() 85 | ev = strings.ToLower(ev) 86 | l, ok := hs.set[ev] 87 | if !ok { 88 | l = &hList{} 89 | } 90 | hn := &hNode{ 91 | set: hs, 92 | event: ev, 93 | handler: h, 94 | } 95 | if !ok { 96 | l.start = hn 97 | } else { 98 | hn.prev = l.end 99 | l.end.next = hn 100 | } 101 | l.end = hn 102 | hs.set[ev] = l 103 | return hn 104 | } 105 | 106 | func (hs *hSet) remove(hn *hNode) { 107 | hs.Lock() 108 | defer hs.Unlock() 109 | l, ok := hs.set[hn.event] 110 | if !ok { 111 | logging.Error("Removing node for unknown event '%s'", hn.event) 112 | return 113 | } 114 | if hn.next == nil { 115 | l.end = hn.prev 116 | } else { 117 | hn.next.prev = hn.prev 118 | } 119 | if hn.prev == nil { 120 | l.start = hn.next 121 | } else { 122 | hn.prev.next = hn.next 123 | } 124 | hn.next = nil 125 | hn.prev = nil 126 | hn.set = nil 127 | if l.start == nil || l.end == nil { 128 | delete(hs.set, hn.event) 129 | } 130 | } 131 | 132 | func (hs *hSet) getHandlers(ev string) []*hNode { 133 | hs.RLock() 134 | defer hs.RUnlock() 135 | list, ok := hs.set[ev] 136 | if !ok { 137 | return nil 138 | } 139 | // Copy current list of handlers to a temporary slice under the lock. 140 | handlers := make([]*hNode, 0) 141 | for hn := list.start; hn != nil; hn = hn.next { 142 | handlers = append(handlers, hn) 143 | } 144 | return handlers 145 | } 146 | 147 | func (hs *hSet) dispatch(conn *Conn, line *Line) { 148 | ev := strings.ToLower(line.Cmd) 149 | wg := &sync.WaitGroup{} 150 | for _, hn := range hs.getHandlers(ev) { 151 | wg.Add(1) 152 | go func(hn *hNode) { 153 | hn.Handle(conn, line.Copy()) 154 | wg.Done() 155 | }(hn) 156 | } 157 | wg.Wait() 158 | } 159 | 160 | // Handle adds the provided handler to the foreground set for the named event. 161 | // It will return a Remover that allows that handler to be removed again. 162 | func (conn *Conn) Handle(name string, h Handler) Remover { 163 | return conn.fgHandlers.add(name, h) 164 | } 165 | 166 | // HandleBG adds the provided handler to the background set for the named 167 | // event. It may go away in the future. 168 | // It will return a Remover that allows that handler to be removed again. 169 | func (conn *Conn) HandleBG(name string, h Handler) Remover { 170 | return conn.bgHandlers.add(name, h) 171 | } 172 | 173 | func (conn *Conn) handle(name string, h Handler) Remover { 174 | return conn.intHandlers.add(name, h) 175 | } 176 | 177 | // HandleFunc adds the provided function as a handler in the foreground set 178 | // for the named event. 179 | // It will return a Remover that allows that handler to be removed again. 180 | func (conn *Conn) HandleFunc(name string, hf HandlerFunc) Remover { 181 | return conn.Handle(name, hf) 182 | } 183 | 184 | func (conn *Conn) dispatch(line *Line) { 185 | // We run the internal handlers first, including all state tracking ones. 186 | // This ensures that user-supplied handlers that use the tracker have a 187 | // consistent view of the connection state in handlers that mutate it. 188 | conn.intHandlers.dispatch(conn, line) 189 | go conn.bgHandlers.dispatch(conn, line) 190 | conn.fgHandlers.dispatch(conn, line) 191 | } 192 | 193 | // LogPanic is used as the default panic catcher for the client. If, like me, 194 | // you are not good with computer, and you'd prefer your bot not to vanish into 195 | // the ether whenever you make unfortunate programming mistakes, you may find 196 | // this useful: it will recover panics from handler code and log the errors. 197 | func (conn *Conn) LogPanic(line *Line) { 198 | if err := recover(); err != nil { 199 | _, f, l, _ := runtime.Caller(2) 200 | logging.Error("%s:%d: panic: %v", f, l, err) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /client/dispatch_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "sync/atomic" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestHandlerSet(t *testing.T) { 10 | // A Conn is needed here because the previous behaviour of passing nil to 11 | // hset.dispatch causes a nil pointer dereference with panic recovery. 12 | c, s := setUp(t) 13 | defer s.tearDown() 14 | 15 | hs := handlerSet() 16 | if len(hs.set) != 0 { 17 | t.Errorf("New set contains things!") 18 | } 19 | 20 | callcount := new(int32) 21 | f := func(_ *Conn, _ *Line) { 22 | atomic.AddInt32(callcount, 1) 23 | } 24 | 25 | // Add one 26 | hn1 := hs.add("ONE", HandlerFunc(f)).(*hNode) 27 | hl, ok := hs.set["one"] 28 | if len(hs.set) != 1 || !ok { 29 | t.Errorf("Set doesn't contain 'one' list after add().") 30 | } 31 | if hn1.set != hs || hn1.event != "one" || hn1.prev != nil || hn1.next != nil { 32 | t.Errorf("First node for 'one' not created correctly") 33 | } 34 | if hl.start != hn1 || hl.end != hn1 { 35 | t.Errorf("Node not added to empty 'one' list correctly.") 36 | } 37 | 38 | // Add another one... 39 | hn2 := hs.add("one", HandlerFunc(f)).(*hNode) 40 | if len(hs.set) != 1 { 41 | t.Errorf("Set contains more than 'one' list after add().") 42 | } 43 | if hn2.set != hs || hn2.event != "one" { 44 | t.Errorf("Second node for 'one' not created correctly") 45 | } 46 | if hn1.prev != nil || hn1.next != hn2 || hn2.prev != hn1 || hn2.next != nil { 47 | t.Errorf("Nodes for 'one' not linked correctly.") 48 | } 49 | if hl.start != hn1 || hl.end != hn2 { 50 | t.Errorf("Node not appended to 'one' list correctly.") 51 | } 52 | 53 | // Add a third one! 54 | hn3 := hs.add("one", HandlerFunc(f)).(*hNode) 55 | if len(hs.set) != 1 { 56 | t.Errorf("Set contains more than 'one' list after add().") 57 | } 58 | if hn3.set != hs || hn3.event != "one" { 59 | t.Errorf("Third node for 'one' not created correctly") 60 | } 61 | if hn1.prev != nil || hn1.next != hn2 || 62 | hn2.prev != hn1 || hn2.next != hn3 || 63 | hn3.prev != hn2 || hn3.next != nil { 64 | t.Errorf("Nodes for 'one' not linked correctly.") 65 | } 66 | if hl.start != hn1 || hl.end != hn3 { 67 | t.Errorf("Node not appended to 'one' list correctly.") 68 | } 69 | 70 | // And finally a fourth one! 71 | hn4 := hs.add("one", HandlerFunc(f)).(*hNode) 72 | if len(hs.set) != 1 { 73 | t.Errorf("Set contains more than 'one' list after add().") 74 | } 75 | if hn4.set != hs || hn4.event != "one" { 76 | t.Errorf("Fourth node for 'one' not created correctly.") 77 | } 78 | if hn1.prev != nil || hn1.next != hn2 || 79 | hn2.prev != hn1 || hn2.next != hn3 || 80 | hn3.prev != hn2 || hn3.next != hn4 || 81 | hn4.prev != hn3 || hn4.next != nil { 82 | t.Errorf("Nodes for 'one' not linked correctly.") 83 | } 84 | if hl.start != hn1 || hl.end != hn4 { 85 | t.Errorf("Node not appended to 'one' list correctly.") 86 | } 87 | 88 | // Dispatch should result in 4 additions. 89 | if atomic.LoadInt32(callcount) != 0 { 90 | t.Errorf("Something incremented call count before we were expecting it.") 91 | } 92 | hs.dispatch(c, &Line{Cmd: "One"}) 93 | <-time.After(time.Millisecond) 94 | if atomic.LoadInt32(callcount) != 4 { 95 | t.Errorf("Our handler wasn't called four times :-(") 96 | } 97 | 98 | // Remove node 3. 99 | hn3.Remove() 100 | if len(hs.set) != 1 { 101 | t.Errorf("Set list count changed after remove().") 102 | } 103 | if hn3.set != nil || hn3.prev != nil || hn3.next != nil { 104 | t.Errorf("Third node for 'one' not removed correctly.") 105 | } 106 | if hn1.prev != nil || hn1.next != hn2 || 107 | hn2.prev != hn1 || hn2.next != hn4 || 108 | hn4.prev != hn2 || hn4.next != nil { 109 | t.Errorf("Third node for 'one' not unlinked correctly.") 110 | } 111 | if hl.start != hn1 || hl.end != hn4 { 112 | t.Errorf("Third node for 'one' changed list pointers.") 113 | } 114 | 115 | // Dispatch should result in 3 additions. 116 | hs.dispatch(c, &Line{Cmd: "One"}) 117 | <-time.After(time.Millisecond) 118 | if atomic.LoadInt32(callcount) != 7 { 119 | t.Errorf("Our handler wasn't called three times :-(") 120 | } 121 | 122 | // Remove node 1. 123 | hs.remove(hn1) 124 | if len(hs.set) != 1 { 125 | t.Errorf("Set list count changed after remove().") 126 | } 127 | if hn1.set != nil || hn1.prev != nil || hn1.next != nil { 128 | t.Errorf("First node for 'one' not removed correctly.") 129 | } 130 | if hn2.prev != nil || hn2.next != hn4 || hn4.prev != hn2 || hn4.next != nil { 131 | t.Errorf("First node for 'one' not unlinked correctly.") 132 | } 133 | if hl.start != hn2 || hl.end != hn4 { 134 | t.Errorf("First node for 'one' didn't change list pointers.") 135 | } 136 | 137 | // Dispatch should result in 2 additions. 138 | hs.dispatch(c, &Line{Cmd: "One"}) 139 | <-time.After(time.Millisecond) 140 | if atomic.LoadInt32(callcount) != 9 { 141 | t.Errorf("Our handler wasn't called two times :-(") 142 | } 143 | 144 | // Remove node 4. 145 | hn4.Remove() 146 | if len(hs.set) != 1 { 147 | t.Errorf("Set list count changed after remove().") 148 | } 149 | if hn4.set != nil || hn4.prev != nil || hn4.next != nil { 150 | t.Errorf("Fourth node for 'one' not removed correctly.") 151 | } 152 | if hn2.prev != nil || hn2.next != nil { 153 | t.Errorf("Fourth node for 'one' not unlinked correctly.") 154 | } 155 | if hl.start != hn2 || hl.end != hn2 { 156 | t.Errorf("Fourth node for 'one' didn't change list pointers.") 157 | } 158 | 159 | // Dispatch should result in 1 addition. 160 | hs.dispatch(c, &Line{Cmd: "One"}) 161 | <-time.After(time.Millisecond) 162 | if atomic.LoadInt32(callcount) != 10 { 163 | t.Errorf("Our handler wasn't called once :-(") 164 | } 165 | 166 | // Remove node 2. 167 | hs.remove(hn2) 168 | if len(hs.set) != 0 { 169 | t.Errorf("Removing last node in 'one' didn't remove list.") 170 | } 171 | if hn2.set != nil || hn2.prev != nil || hn2.next != nil { 172 | t.Errorf("Second node for 'one' not removed correctly.") 173 | } 174 | if hl.start != nil || hl.end != nil { 175 | t.Errorf("Second node for 'one' didn't change list pointers.") 176 | } 177 | 178 | // Dispatch should result in NO additions. 179 | hs.dispatch(c, &Line{Cmd: "One"}) 180 | <-time.After(time.Millisecond) 181 | if atomic.LoadInt32(callcount) != 10 { 182 | t.Errorf("Our handler was called?") 183 | } 184 | } 185 | 186 | func TestPanicRecovery(t *testing.T) { 187 | c, s := setUp(t) 188 | defer s.tearDown() 189 | 190 | recovered := callCheck(t) 191 | c.cfg.Recover = func(conn *Conn, line *Line) { 192 | if err, ok := recover().(string); ok && err == "panic!" { 193 | recovered.call() 194 | } 195 | } 196 | c.HandleFunc(PRIVMSG, func(conn *Conn, line *Line) { 197 | panic("panic!") 198 | }) 199 | c.in <- ParseLine(":nick!user@host.com PRIVMSG #channel :OH NO PIGEONS") 200 | recovered.assertWasCalled("Failed to recover panic!") 201 | } 202 | -------------------------------------------------------------------------------- /client/doc.go: -------------------------------------------------------------------------------- 1 | // Package client implements an IRC client. It handles protocol basics 2 | // such as initial connection and responding to server PINGs, and has 3 | // optional state tracking support which will keep tabs on every nick 4 | // present in the same channels as the client. Other features include 5 | // SSL support, automatic splitting of long lines, and panic recovery 6 | // for handlers. 7 | // 8 | // Incoming IRC messages are parsed into client.Line structs and trigger 9 | // events based on the IRC verb (e.g. PRIVMSG) of the message. Handlers 10 | // for these events conform to the client.Handler interface; a HandlerFunc 11 | // type to wrap bare functions is provided a-la the net/http package. 12 | // 13 | // Creating a client, adding a handler and connecting to a server looks 14 | // soemthing like this, for the simple case: 15 | // 16 | // // Create a new client, which will connect with the nick "myNick" 17 | // irc := client.SimpleClient("myNick") 18 | // 19 | // // Add a handler that waits for the "disconnected" event and 20 | // // closes a channel to signal everything is done. 21 | // disconnected := make(chan struct{}) 22 | // c.HandleFunc("disconnected", func(c *client.Conn, l *client.Line) { 23 | // close(disconnected) 24 | // }) 25 | // 26 | // // Connect to an IRC server. 27 | // if err := c.ConnectTo("irc.freenode.net"); err != nil { 28 | // log.Fatalf("Connection error: %v\n", err) 29 | // } 30 | // 31 | // // Wait for disconnection. 32 | // <-disconnected 33 | // 34 | package client 35 | -------------------------------------------------------------------------------- /client/handlers.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | // this file contains the basic set of event handlers 4 | // to manage tracking an irc connection etc. 5 | 6 | import ( 7 | "sort" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "encoding/base64" 13 | "github.com/fluffle/goirc/logging" 14 | ) 15 | 16 | // saslCap is the IRCv3 capability used for SASL authentication. 17 | const saslCap = "sasl" 18 | 19 | // sets up the internal event handlers to do essential IRC protocol things 20 | var intHandlers = map[string]HandlerFunc{ 21 | REGISTER: (*Conn).h_REGISTER, 22 | "001": (*Conn).h_001, 23 | "433": (*Conn).h_433, 24 | CTCP: (*Conn).h_CTCP, 25 | NICK: (*Conn).h_NICK, 26 | PING: (*Conn).h_PING, 27 | CAP: (*Conn).h_CAP, 28 | "410": (*Conn).h_410, 29 | AUTHENTICATE: (*Conn).h_AUTHENTICATE, 30 | "903": (*Conn).h_903, 31 | "904": (*Conn).h_904, 32 | "908": (*Conn).h_908, 33 | } 34 | 35 | // set up the ircv3 capabilities supported by this client which will be requested by default to the server. 36 | var defaultCaps = []string{} 37 | 38 | func (conn *Conn) addIntHandlers() { 39 | for n, h := range intHandlers { 40 | // internal handlers are essential for the IRC client 41 | // to function, so we don't save their Removers here 42 | conn.handle(n, h) 43 | } 44 | } 45 | 46 | // Basic ping/pong handler 47 | func (conn *Conn) h_PING(line *Line) { 48 | conn.Pong(line.Args[0]) 49 | } 50 | 51 | // Handler for initial registration with server once tcp connection is made. 52 | func (conn *Conn) h_REGISTER(line *Line) { 53 | if conn.cfg.EnableCapabilityNegotiation { 54 | conn.Cap(CAP_LS) 55 | } 56 | 57 | if conn.cfg.Pass != "" { 58 | conn.Pass(conn.cfg.Pass) 59 | } 60 | conn.Nick(conn.cfg.Me.Nick) 61 | conn.User(conn.cfg.Me.Ident, conn.cfg.Me.Name) 62 | } 63 | 64 | func (conn *Conn) getRequestCapabilities() *capSet { 65 | s := capabilitySet() 66 | 67 | // add capabilites supported by the client 68 | s.Add(defaultCaps...) 69 | 70 | if conn.cfg.Sasl != nil { 71 | // add the SASL cap if enabled 72 | s.Add(saslCap) 73 | } 74 | 75 | // add capabilites requested by the user 76 | s.Add(conn.cfg.Capabilites...) 77 | 78 | return s 79 | } 80 | 81 | func (conn *Conn) negotiateCapabilities(supportedCaps []string) { 82 | conn.supportedCaps.Add(supportedCaps...) 83 | 84 | reqCaps := conn.getRequestCapabilities() 85 | reqCaps.Intersect(conn.supportedCaps) 86 | 87 | if reqCaps.Size() > 0 { 88 | conn.Cap(CAP_REQ, reqCaps.Slice()...) 89 | } else { 90 | conn.Cap(CAP_END) 91 | } 92 | } 93 | 94 | func (conn *Conn) handleCapAck(caps []string) { 95 | gotSasl := false 96 | for _, cap := range caps { 97 | conn.currCaps.Add(cap) 98 | 99 | if conn.cfg.Sasl != nil && cap == saslCap { 100 | mech, ir, err := conn.cfg.Sasl.Start() 101 | 102 | if err != nil { 103 | logging.Warn("SASL authentication failed: %v", err) 104 | continue 105 | } 106 | 107 | // TODO: when IRC 3.2 capability negotiation is supported, ensure the 108 | // capability value is used to match the chosen mechanism 109 | 110 | gotSasl = true 111 | conn.saslRemainingData = ir 112 | 113 | conn.Authenticate(mech) 114 | } 115 | } 116 | 117 | if !gotSasl { 118 | conn.Cap(CAP_END) 119 | } 120 | } 121 | 122 | func (conn *Conn) handleCapNak(caps []string) { 123 | conn.Cap(CAP_END) 124 | } 125 | 126 | const ( 127 | CAP_LS = "LS" 128 | CAP_REQ = "REQ" 129 | CAP_ACK = "ACK" 130 | CAP_NAK = "NAK" 131 | CAP_END = "END" 132 | ) 133 | 134 | type capSet struct { 135 | caps map[string]bool 136 | mu sync.RWMutex 137 | } 138 | 139 | func capabilitySet() *capSet { 140 | return &capSet{ 141 | caps: make(map[string]bool), 142 | } 143 | } 144 | 145 | func (c *capSet) Add(caps ...string) { 146 | c.mu.Lock() 147 | for _, cap := range caps { 148 | if strings.HasPrefix(cap, "-") { 149 | c.caps[cap[1:]] = false 150 | } else { 151 | c.caps[cap] = true 152 | } 153 | } 154 | c.mu.Unlock() 155 | } 156 | 157 | func (c *capSet) Has(cap string) bool { 158 | c.mu.RLock() 159 | defer c.mu.RUnlock() 160 | return c.caps[cap] 161 | } 162 | 163 | // Intersect computes the intersection of two sets. 164 | func (c *capSet) Intersect(other *capSet) { 165 | c.mu.Lock() 166 | 167 | for cap := range c.caps { 168 | if !other.Has(cap) { 169 | delete(c.caps, cap) 170 | } 171 | } 172 | 173 | c.mu.Unlock() 174 | } 175 | 176 | func (c *capSet) Slice() []string { 177 | c.mu.RLock() 178 | defer c.mu.RUnlock() 179 | 180 | capSlice := make([]string, 0, len(c.caps)) 181 | for cap := range c.caps { 182 | capSlice = append(capSlice, cap) 183 | } 184 | 185 | // make output predictable for testing 186 | sort.Strings(capSlice) 187 | return capSlice 188 | } 189 | 190 | func (c *capSet) Size() int { 191 | c.mu.RLock() 192 | defer c.mu.RUnlock() 193 | return len(c.caps) 194 | } 195 | 196 | // This handler is triggered when an invalid cap command is received by the server. 197 | func (conn *Conn) h_410(line *Line) { 198 | logging.Warn("Invalid cap subcommand: ", line.Args[1]) 199 | } 200 | 201 | // Handler for capability negotiation commands. 202 | // Note that even if multiple CAP_END commands may be sent to the server during negotiation, 203 | // only the first will be considered. 204 | func (conn *Conn) h_CAP(line *Line) { 205 | subcommand := line.Args[1] 206 | 207 | caps := strings.Fields(line.Text()) 208 | switch subcommand { 209 | case CAP_LS: 210 | conn.negotiateCapabilities(caps) 211 | case CAP_ACK: 212 | conn.handleCapAck(caps) 213 | case CAP_NAK: 214 | conn.handleCapNak(caps) 215 | } 216 | } 217 | 218 | // Handler for SASL authentication 219 | func (conn *Conn) h_AUTHENTICATE(line *Line) { 220 | if conn.cfg.Sasl == nil { 221 | return 222 | } 223 | 224 | if conn.saslRemainingData != nil { 225 | data := "+" // plus sign representing empty data 226 | if len(conn.saslRemainingData) > 0 { 227 | data = base64.StdEncoding.EncodeToString(conn.saslRemainingData) 228 | } 229 | 230 | // TODO: batch data into chunks of 400 bytes per the spec 231 | 232 | conn.Authenticate(data) 233 | conn.saslRemainingData = nil 234 | return 235 | } 236 | 237 | // TODO: handle data over 400 bytes long (which will be chunked into multiple messages per the spec) 238 | challenge, err := base64.StdEncoding.DecodeString(line.Args[0]) 239 | if err != nil { 240 | logging.Error("Failed to decode SASL challenge: %v", err) 241 | return 242 | } 243 | 244 | response, err := conn.cfg.Sasl.Next(challenge) 245 | if err != nil { 246 | logging.Error("Failed to generate response for SASL challenge: %v", err) 247 | return 248 | } 249 | 250 | // TODO: batch data into chunks of 400 bytes per the spec 251 | data := base64.StdEncoding.EncodeToString(response) 252 | conn.Authenticate(data) 253 | } 254 | 255 | // Handler for RPL_SASLSUCCESS. 256 | func (conn *Conn) h_903(line *Line) { 257 | conn.Cap(CAP_END) 258 | } 259 | 260 | // Handler for RPL_SASLFAILURE. 261 | func (conn *Conn) h_904(line *Line) { 262 | logging.Warn("SASL authentication failed") 263 | conn.Cap(CAP_END) 264 | } 265 | 266 | // Handler for RPL_SASLMECHS. 267 | func (conn *Conn) h_908(line *Line) { 268 | logging.Warn("SASL mechanism not supported, supported mechanisms are: %v", line.Args[1]) 269 | conn.Cap(CAP_END) 270 | } 271 | 272 | // Handler to trigger a CONNECTED event on receipt of numeric 001 273 | // : 001 :Welcome message !@ 274 | func (conn *Conn) h_001(line *Line) { 275 | // We're connected! Defer this for control flow reasons. 276 | defer conn.dispatch(&Line{Cmd: CONNECTED, Time: time.Now()}) 277 | 278 | // Accept the server's opinion of what our nick actually is 279 | // and record our ident and hostname (from the server's perspective) 280 | me, nick, t := conn.Me(), line.Target(), line.Text() 281 | if idx := strings.LastIndex(t, " "); idx != -1 { 282 | t = t[idx+1:] 283 | } 284 | _, ident, host, ok := parseUserHost(t) 285 | 286 | if me.Nick != nick { 287 | logging.Warn("Server changed our nick on connect: old=%q new=%q", me.Nick, nick) 288 | } 289 | if conn.st != nil { 290 | if ok { 291 | conn.st.NickInfo(me.Nick, ident, host, me.Name) 292 | } 293 | conn.cfg.Me = conn.st.ReNick(me.Nick, nick) 294 | } else { 295 | conn.cfg.Me.Nick = nick 296 | if ok { 297 | conn.cfg.Me.Ident = ident 298 | conn.cfg.Me.Host = host 299 | } 300 | } 301 | } 302 | 303 | // XXX: do we need 005 protocol support message parsing here? 304 | // probably in the future, but I can't quite be arsed yet. 305 | /* 306 | :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 307 | :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 308 | :irc.pl0rt.org 005 GoTest STATUSMSG=~&@%+ EXCEPTS INVEX :are supported by this server 309 | */ 310 | 311 | // Handler to deal with "433 :Nickname already in use" 312 | func (conn *Conn) h_433(line *Line) { 313 | // Args[1] is the new nick we were attempting to acquire 314 | me := conn.Me() 315 | neu := conn.cfg.NewNick(line.Args[1]) 316 | conn.Nick(neu) 317 | if !line.argslen(1) { 318 | return 319 | } 320 | // if this is happening before we're properly connected (i.e. the nick 321 | // we sent in the initial NICK command is in use) we will not receive 322 | // a NICK message to confirm our change of nick, so ReNick here... 323 | if line.Args[1] == me.Nick { 324 | if conn.st != nil { 325 | conn.cfg.Me = conn.st.ReNick(me.Nick, neu) 326 | } else { 327 | conn.cfg.Me.Nick = neu 328 | } 329 | } 330 | } 331 | 332 | // Handle VERSION requests and CTCP PING 333 | func (conn *Conn) h_CTCP(line *Line) { 334 | if line.Args[0] == VERSION { 335 | conn.CtcpReply(line.Nick, VERSION, conn.cfg.Version) 336 | } else if line.Args[0] == PING && line.argslen(2) { 337 | conn.CtcpReply(line.Nick, PING, line.Args[2]) 338 | } 339 | } 340 | 341 | // Handle updating our own NICK if we're not using the state tracker 342 | func (conn *Conn) h_NICK(line *Line) { 343 | if conn.st == nil && line.Nick == conn.cfg.Me.Nick { 344 | conn.cfg.Me.Nick = line.Args[0] 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /client/handlers_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/fluffle/goirc/state" 8 | "github.com/golang/mock/gomock" 9 | ) 10 | 11 | // This test performs a simple end-to-end verification of correct line parsing 12 | // and event dispatch as well as testing the PING handler. All the other tests 13 | // in this file will call their respective handlers synchronously, otherwise 14 | // testing becomes more difficult. 15 | func TestPING(t *testing.T) { 16 | _, s := setUp(t) 17 | defer s.tearDown() 18 | s.nc.Send("PING :1234567890") 19 | s.nc.Expect("PONG :1234567890") 20 | } 21 | 22 | // Test the REGISTER handler matches section 3.1 of rfc2812 23 | func TestREGISTER(t *testing.T) { 24 | c, s := setUp(t) 25 | defer s.tearDown() 26 | 27 | c.h_REGISTER(&Line{Cmd: REGISTER}) 28 | s.nc.Expect("NICK test") 29 | s.nc.Expect("USER test 12 * :Testing IRC") 30 | s.nc.ExpectNothing() 31 | 32 | c.cfg.Pass = "12345" 33 | c.cfg.Me.Ident = "idiot" 34 | c.cfg.Me.Name = "I've got the same combination on my luggage!" 35 | c.h_REGISTER(&Line{Cmd: REGISTER}) 36 | s.nc.Expect("PASS 12345") 37 | s.nc.Expect("NICK test") 38 | s.nc.Expect("USER idiot 12 * :I've got the same combination on my luggage!") 39 | s.nc.ExpectNothing() 40 | } 41 | 42 | // Test the handler for 001 / RPL_WELCOME 43 | func Test001(t *testing.T) { 44 | c, s := setUp(t) 45 | defer s.tearDown() 46 | 47 | l := ParseLine(":irc.server.org 001 newnick :Welcome to IRC newnick!ident@somehost.com") 48 | // Set up a handler to detect whether connected handler is called from 001 49 | hcon := false 50 | c.HandleFunc("connected", func(conn *Conn, line *Line) { 51 | hcon = true 52 | }) 53 | 54 | // Test state tracking first. 55 | gomock.InOrder( 56 | s.st.EXPECT().Me().Return(c.cfg.Me), 57 | s.st.EXPECT().NickInfo("test", "ident", "somehost.com", "Testing IRC"), 58 | s.st.EXPECT().ReNick("test", "newnick").Return(&state.Nick{ 59 | Nick: "newnick", 60 | Ident: c.cfg.Me.Ident, 61 | Name: c.cfg.Me.Name, 62 | }), 63 | ) 64 | // Call handler with a valid 001 line 65 | c.h_001(l) 66 | <-time.After(time.Millisecond) 67 | if !hcon { 68 | t.Errorf("001 handler did not dispatch connected event.") 69 | } 70 | 71 | // Now without state tracking. 72 | c.st = nil 73 | c.h_001(l) 74 | // Check host parsed correctly 75 | if c.cfg.Me.Host != "somehost.com" { 76 | t.Errorf("Host parsing failed, host is '%s'.", c.cfg.Me.Host) 77 | } 78 | c.st = s.st 79 | } 80 | 81 | // Test the handler for 433 / ERR_NICKNAMEINUSE 82 | func Test433(t *testing.T) { 83 | c, s := setUp(t) 84 | defer s.tearDown() 85 | 86 | // Call handler with a 433 line, not triggering c.cfg.Me.Renick() 87 | s.st.EXPECT().Me().Return(c.cfg.Me) 88 | c.h_433(ParseLine(":irc.server.org 433 test new :Nickname is already in use.")) 89 | s.nc.Expect("NICK " + DefaultNewNick("new")) 90 | 91 | // Send a line that will trigger a renick. This happens when our wanted 92 | // nick is unavailable during initial negotiation, so we must choose a 93 | // different one before the connection can proceed. No NICK line will be 94 | // sent by the server to confirm nick change in this case. 95 | want := DefaultNewNick(c.cfg.Me.Nick) 96 | gomock.InOrder( 97 | s.st.EXPECT().Me().Return(c.cfg.Me), 98 | s.st.EXPECT().ReNick("test", want).Return(c.cfg.Me), 99 | ) 100 | c.h_433(ParseLine(":irc.server.org 433 test test :Nickname is already in use.")) 101 | s.nc.Expect("NICK " + want) 102 | 103 | // Test the code path that *doesn't* involve state tracking. 104 | c.st = nil 105 | c.h_433(ParseLine(":irc.server.org 433 test test :Nickname is already in use.")) 106 | s.nc.Expect("NICK " + want) 107 | 108 | if c.cfg.Me.Nick != want { 109 | t.Errorf("My nick not updated from '%s'.", c.cfg.Me.Nick) 110 | } 111 | c.st = s.st 112 | } 113 | 114 | // Test the handler for NICK messages when state tracking is disabled 115 | func TestNICK(t *testing.T) { 116 | c, s := setUp(t) 117 | defer s.tearDown() 118 | 119 | // State tracking is enabled by default in setUp 120 | c.st = nil 121 | 122 | // Call handler with a NICK line changing "our" nick to test1. 123 | c.h_NICK(ParseLine(":test!test@somehost.com NICK :test1")) 124 | 125 | // Verify that our Nick has changed 126 | if c.cfg.Me.Nick != "test1" { 127 | t.Errorf("NICK did not result in changing our nick.") 128 | } 129 | 130 | // Send a NICK line for something that isn't us. 131 | c.h_NICK(ParseLine(":blah!moo@cows.com NICK :milk")) 132 | 133 | // Verify that our Nick hasn't changed 134 | if c.cfg.Me.Nick != "test1" { 135 | t.Errorf("NICK did not result in changing our nick.") 136 | } 137 | 138 | // Re-enable state tracking and send a line that *should* change nick. 139 | c.st = s.st 140 | c.h_NICK(ParseLine(":test1!test@somehost.com NICK :test2")) 141 | 142 | // Verify that our Nick hasn't changed (should be handled by h_STNICK). 143 | if c.cfg.Me.Nick != "test1" { 144 | t.Errorf("NICK changed our nick when state tracking enabled.") 145 | } 146 | } 147 | 148 | // Test the handler for CTCP messages 149 | func TestCTCP(t *testing.T) { 150 | c, s := setUp(t) 151 | defer s.tearDown() 152 | 153 | // Call handler with CTCP VERSION 154 | c.h_CTCP(ParseLine(":blah!moo@cows.com PRIVMSG test :\001VERSION\001")) 155 | 156 | // Expect a version reply 157 | s.nc.Expect("NOTICE blah :\001VERSION Powered by GoIRC\001") 158 | 159 | // Call handler with CTCP PING 160 | c.h_CTCP(ParseLine(":blah!moo@cows.com PRIVMSG test :\001PING 1234567890\001")) 161 | 162 | // Expect a ping reply 163 | s.nc.Expect("NOTICE blah :\001PING 1234567890\001") 164 | 165 | // Call handler with CTCP UNKNOWN 166 | c.h_CTCP(ParseLine(":blah!moo@cows.com PRIVMSG test :\001UNKNOWN ctcp\001")) 167 | } 168 | 169 | // Test the handler for JOIN messages 170 | func TestJOIN(t *testing.T) { 171 | c, s := setUp(t) 172 | defer s.tearDown() 173 | 174 | // The state tracker should be creating a new channel in this first test 175 | chan1 := &state.Channel{Name: "#test1"} 176 | 177 | gomock.InOrder( 178 | s.st.EXPECT().GetChannel("#test1").Return(nil), 179 | s.st.EXPECT().GetNick("test").Return(c.cfg.Me), 180 | s.st.EXPECT().Me().Return(c.cfg.Me), 181 | s.st.EXPECT().NewChannel("#test1").Return(chan1), 182 | s.st.EXPECT().Associate("#test1", "test"), 183 | ) 184 | 185 | // Use #test1 to test expected behaviour 186 | // Call handler with JOIN by test to #test1 187 | c.h_JOIN(ParseLine(":test!test@somehost.com JOIN :#test1")) 188 | 189 | // Verify that the MODE and WHO commands are sent correctly 190 | s.nc.Expect("MODE #test1") 191 | s.nc.Expect("WHO #test1") 192 | 193 | // In this second test, we should be creating a new nick 194 | nick1 := &state.Nick{Nick: "user1"} 195 | 196 | gomock.InOrder( 197 | s.st.EXPECT().GetChannel("#test1").Return(chan1), 198 | s.st.EXPECT().GetNick("user1").Return(nil), 199 | s.st.EXPECT().NewNick("user1").Return(nick1), 200 | s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "").Return(nick1), 201 | s.st.EXPECT().Associate("#test1", "user1"), 202 | ) 203 | 204 | // OK, now #test1 exists, JOIN another user we don't know about 205 | c.h_JOIN(ParseLine(":user1!ident1@host1.com JOIN :#test1")) 206 | 207 | // Verify that the WHO command is sent correctly 208 | s.nc.Expect("WHO user1") 209 | 210 | // In this third test, we'll be pretending we know about the nick already. 211 | nick2 := &state.Nick{Nick: "user2"} 212 | gomock.InOrder( 213 | s.st.EXPECT().GetChannel("#test1").Return(chan1), 214 | s.st.EXPECT().GetNick("user2").Return(nick2), 215 | s.st.EXPECT().Associate("#test1", "user2"), 216 | ) 217 | c.h_JOIN(ParseLine(":user2!ident2@host2.com JOIN :#test1")) 218 | 219 | // Test error paths 220 | gomock.InOrder( 221 | // unknown channel, unknown nick 222 | s.st.EXPECT().GetChannel("#test2").Return(nil), 223 | s.st.EXPECT().GetNick("blah").Return(nil), 224 | s.st.EXPECT().Me().Return(c.cfg.Me), 225 | // unknown channel, known nick that isn't Me. 226 | s.st.EXPECT().GetChannel("#test2").Return(nil), 227 | s.st.EXPECT().GetNick("user2").Return(nick2), 228 | s.st.EXPECT().Me().Return(c.cfg.Me), 229 | ) 230 | c.h_JOIN(ParseLine(":blah!moo@cows.com JOIN :#test2")) 231 | c.h_JOIN(ParseLine(":user2!ident2@host2.com JOIN :#test2")) 232 | } 233 | 234 | // Test the handler for PART messages 235 | func TestPART(t *testing.T) { 236 | c, s := setUp(t) 237 | defer s.tearDown() 238 | 239 | // PART should dissociate a nick from a channel. 240 | s.st.EXPECT().Dissociate("#test1", "user1") 241 | c.h_PART(ParseLine(":user1!ident1@host1.com PART #test1 :Bye!")) 242 | } 243 | 244 | // Test the handler for KICK messages 245 | // (this is very similar to the PART message test) 246 | func TestKICK(t *testing.T) { 247 | c, s := setUp(t) 248 | defer s.tearDown() 249 | 250 | // KICK should dissociate a nick from a channel. 251 | s.st.EXPECT().Dissociate("#test1", "user1") 252 | c.h_KICK(ParseLine(":test!test@somehost.com KICK #test1 user1 :Bye!")) 253 | } 254 | 255 | // Test the handler for QUIT messages 256 | func TestQUIT(t *testing.T) { 257 | c, s := setUp(t) 258 | defer s.tearDown() 259 | 260 | // Have user1 QUIT. All possible errors handled by state tracker \o/ 261 | s.st.EXPECT().DelNick("user1") 262 | c.h_QUIT(ParseLine(":user1!ident1@host1.com QUIT :Bye!")) 263 | } 264 | 265 | // Test the handler for MODE messages 266 | func TestMODE(t *testing.T) { 267 | c, s := setUp(t) 268 | defer s.tearDown() 269 | 270 | // Channel modes 271 | gomock.InOrder( 272 | s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}), 273 | s.st.EXPECT().ChannelModes("#test1", "+sk", "somekey"), 274 | ) 275 | c.h_MODE(ParseLine(":user1!ident1@host1.com MODE #test1 +sk somekey")) 276 | 277 | // Nick modes for Me. 278 | gomock.InOrder( 279 | s.st.EXPECT().GetChannel("test").Return(nil), 280 | s.st.EXPECT().GetNick("test").Return(c.cfg.Me), 281 | s.st.EXPECT().Me().Return(c.cfg.Me), 282 | s.st.EXPECT().NickModes("test", "+i"), 283 | ) 284 | c.h_MODE(ParseLine(":test!test@somehost.com MODE test +i")) 285 | 286 | // Check error paths 287 | gomock.InOrder( 288 | // send a valid user mode that's not us 289 | s.st.EXPECT().GetChannel("user1").Return(nil), 290 | s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}), 291 | s.st.EXPECT().Me().Return(c.cfg.Me), 292 | // Send a random mode for an unknown channel 293 | s.st.EXPECT().GetChannel("#test2").Return(nil), 294 | s.st.EXPECT().GetNick("#test2").Return(nil), 295 | ) 296 | c.h_MODE(ParseLine(":user1!ident1@host1.com MODE user1 +w")) 297 | c.h_MODE(ParseLine(":user1!ident1@host1.com MODE #test2 +is")) 298 | } 299 | 300 | // Test the handler for TOPIC messages 301 | func TestTOPIC(t *testing.T) { 302 | c, s := setUp(t) 303 | defer s.tearDown() 304 | 305 | // Ensure TOPIC reply calls Topic 306 | gomock.InOrder( 307 | s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}), 308 | s.st.EXPECT().Topic("#test1", "something something"), 309 | ) 310 | c.h_TOPIC(ParseLine(":user1!ident1@host1.com TOPIC #test1 :something something")) 311 | 312 | // Check error paths -- send a topic for an unknown channel 313 | s.st.EXPECT().GetChannel("#test2").Return(nil) 314 | c.h_TOPIC(ParseLine(":user1!ident1@host1.com TOPIC #test2 :dark side")) 315 | } 316 | 317 | // Test the handler for 311 / RPL_WHOISUSER 318 | func Test311(t *testing.T) { 319 | c, s := setUp(t) 320 | defer s.tearDown() 321 | 322 | // Ensure 311 reply calls NickInfo 323 | gomock.InOrder( 324 | s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}), 325 | s.st.EXPECT().Me().Return(c.cfg.Me), 326 | s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "name"), 327 | ) 328 | c.h_311(ParseLine(":irc.server.org 311 test user1 ident1 host1.com * :name")) 329 | 330 | // Check error paths -- send a 311 for an unknown nick 331 | s.st.EXPECT().GetNick("user2").Return(nil) 332 | c.h_311(ParseLine(":irc.server.org 311 test user2 ident2 host2.com * :dongs")) 333 | } 334 | 335 | // Test the handler for 324 / RPL_CHANNELMODEIS 336 | func Test324(t *testing.T) { 337 | c, s := setUp(t) 338 | defer s.tearDown() 339 | 340 | // Ensure 324 reply calls ChannelModes 341 | gomock.InOrder( 342 | s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}), 343 | s.st.EXPECT().ChannelModes("#test1", "+sk", "somekey"), 344 | ) 345 | c.h_324(ParseLine(":irc.server.org 324 test #test1 +sk somekey")) 346 | 347 | // Check error paths -- send 324 for an unknown channel 348 | s.st.EXPECT().GetChannel("#test2").Return(nil) 349 | c.h_324(ParseLine(":irc.server.org 324 test #test2 +pmt")) 350 | } 351 | 352 | // Test the handler for 332 / RPL_TOPIC 353 | func Test332(t *testing.T) { 354 | c, s := setUp(t) 355 | defer s.tearDown() 356 | 357 | // Ensure 332 reply calls Topic 358 | gomock.InOrder( 359 | s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}), 360 | s.st.EXPECT().Topic("#test1", "something something"), 361 | ) 362 | c.h_332(ParseLine(":irc.server.org 332 test #test1 :something something")) 363 | 364 | // Check error paths -- send 332 for an unknown channel 365 | s.st.EXPECT().GetChannel("#test2").Return(nil) 366 | c.h_332(ParseLine(":irc.server.org 332 test #test2 :dark side")) 367 | } 368 | 369 | // Test the handler for 352 / RPL_WHOREPLY 370 | func Test352(t *testing.T) { 371 | c, s := setUp(t) 372 | defer s.tearDown() 373 | 374 | // Ensure 352 reply calls NickInfo and NickModes 375 | gomock.InOrder( 376 | s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}), 377 | s.st.EXPECT().Me().Return(c.cfg.Me), 378 | s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "name"), 379 | ) 380 | c.h_352(ParseLine(":irc.server.org 352 test #test1 ident1 host1.com irc.server.org user1 G :0 name")) 381 | 382 | // Check that modes are set correctly from WHOREPLY 383 | gomock.InOrder( 384 | s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}), 385 | s.st.EXPECT().Me().Return(c.cfg.Me), 386 | s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "name"), 387 | s.st.EXPECT().NickModes("user1", "+o"), 388 | s.st.EXPECT().NickModes("user1", "+i"), 389 | ) 390 | c.h_352(ParseLine(":irc.server.org 352 test #test1 ident1 host1.com irc.server.org user1 H* :0 name")) 391 | 392 | // Check error paths -- send a 352 for an unknown nick 393 | s.st.EXPECT().GetNick("user2").Return(nil) 394 | c.h_352(ParseLine(":irc.server.org 352 test #test2 ident2 host2.com irc.server.org user2 G :0 fooo")) 395 | } 396 | 397 | // Test the handler for 353 / RPL_NAMREPLY 398 | func Test353(t *testing.T) { 399 | c, s := setUp(t) 400 | defer s.tearDown() 401 | 402 | // 353 handler is called twice, so GetChannel will be called twice 403 | s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}).Times(2) 404 | gomock.InOrder( 405 | // "test" is Me, i am known, and already on the channel 406 | s.st.EXPECT().GetNick("test").Return(c.cfg.Me), 407 | s.st.EXPECT().IsOn("#test1", "test").Return(&state.ChanPrivs{}, true), 408 | // user1 is known, but not on the channel, so should be associated 409 | s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}), 410 | s.st.EXPECT().IsOn("#test1", "user1").Return(nil, false), 411 | s.st.EXPECT().Associate("#test1", "user1").Return(&state.ChanPrivs{}), 412 | s.st.EXPECT().ChannelModes("#test1", "+o", "user1"), 413 | ) 414 | for n, m := range map[string]string{ 415 | "user2": "", 416 | "voice": "+v", 417 | "halfop": "+h", 418 | "op": "+o", 419 | "admin": "+a", 420 | "owner": "+q", 421 | } { 422 | calls := []*gomock.Call{ 423 | s.st.EXPECT().GetNick(n).Return(nil), 424 | s.st.EXPECT().NewNick(n).Return(&state.Nick{Nick: n}), 425 | s.st.EXPECT().IsOn("#test1", n).Return(nil, false), 426 | s.st.EXPECT().Associate("#test1", n).Return(&state.ChanPrivs{}), 427 | } 428 | if m != "" { 429 | calls = append(calls, s.st.EXPECT().ChannelModes("#test1", m, n)) 430 | } 431 | gomock.InOrder(calls...) 432 | } 433 | 434 | // Send a couple of names replies (complete with trailing space) 435 | c.h_353(ParseLine(":irc.server.org 353 test = #test1 :test @user1 user2 +voice ")) 436 | c.h_353(ParseLine(":irc.server.org 353 test = #test1 :%halfop @op &admin ~owner ")) 437 | 438 | // Check error paths -- send 353 for an unknown channel 439 | s.st.EXPECT().GetChannel("#test2").Return(nil) 440 | c.h_353(ParseLine(":irc.server.org 353 test = #test2 :test ~user3")) 441 | } 442 | 443 | // Test the handler for 671 (unreal specific) 444 | func Test671(t *testing.T) { 445 | c, s := setUp(t) 446 | defer s.tearDown() 447 | 448 | // Ensure 671 reply calls NickModes 449 | gomock.InOrder( 450 | s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}), 451 | s.st.EXPECT().NickModes("user1", "+z"), 452 | ) 453 | c.h_671(ParseLine(":irc.server.org 671 test user1 :some ignored text")) 454 | 455 | // Check error paths -- send a 671 for an unknown nick 456 | s.st.EXPECT().GetNick("user2").Return(nil) 457 | c.h_671(ParseLine(":irc.server.org 671 test user2 :some ignored text")) 458 | } 459 | 460 | func TestCap(t *testing.T) { 461 | c, s := setUp(t) 462 | defer s.tearDown() 463 | 464 | c.Config().EnableCapabilityNegotiation = true 465 | c.Config().Capabilites = []string{"cap1", "cap2", "cap3", "cap4"} 466 | 467 | c.h_REGISTER(&Line{Cmd: REGISTER}) 468 | s.nc.Expect("CAP LS") 469 | s.nc.Expect("NICK test") 470 | s.nc.Expect("USER test 12 * :Testing IRC") 471 | 472 | // Ensure that capabilities not supported by the server are not requested 473 | s.nc.Send("CAP * LS :cap2 cap4") 474 | s.nc.Expect("CAP REQ :cap2 cap4") 475 | 476 | s.nc.Send("CAP * ACK :cap2 cap4") 477 | s.nc.Expect("CAP END") 478 | 479 | for _, cap := range []string{"cap2", "cap4"} { 480 | if !c.SupportsCapability(cap) { 481 | t.Fail() 482 | } 483 | 484 | if !c.HasCapability(cap) { 485 | t.Fail() 486 | } 487 | } 488 | 489 | for _, cap := range []string{"cap1", "cap3"} { 490 | if c.HasCapability(cap) { 491 | t.Fail() 492 | } 493 | } 494 | 495 | // test disable capability after registration 496 | s.c.Cap("REQ", "-cap4") 497 | s.nc.Expect("CAP REQ :-cap4") 498 | 499 | s.nc.Send("CAP * ACK :-cap4") 500 | s.nc.Expect("CAP END") 501 | 502 | if !c.HasCapability("cap2") { 503 | t.Fail() 504 | } 505 | 506 | if c.HasCapability("cap4") { 507 | t.Fail() 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /client/line.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "runtime" 5 | "strings" 6 | "time" 7 | 8 | "github.com/fluffle/goirc/logging" 9 | ) 10 | 11 | var tagsReplacer = strings.NewReplacer("\\:", ";", "\\s", " ", "\\r", "\r", "\\n", "\n") 12 | 13 | // We parse an incoming line into this struct. Line.Cmd is used as the trigger 14 | // name for incoming event handlers and is the IRC verb, the first sequence 15 | // of non-whitespace characters after ":nick!user@host", e.g. PRIVMSG. 16 | // Raw =~ ":nick!user@host cmd args[] :text" 17 | // Src == "nick!user@host" 18 | // Cmd == e.g. PRIVMSG, 332 19 | type Line struct { 20 | Tags map[string]string 21 | Nick, Ident, Host, Src string 22 | Cmd, Raw string 23 | Args []string 24 | Time time.Time 25 | } 26 | 27 | // Copy returns a deep copy of the Line. 28 | func (l *Line) Copy() *Line { 29 | nl := *l 30 | nl.Args = make([]string, len(l.Args)) 31 | copy(nl.Args, l.Args) 32 | if l.Tags != nil { 33 | nl.Tags = make(map[string]string) 34 | for k, v := range l.Tags { 35 | nl.Tags[k] = v 36 | } 37 | } 38 | return &nl 39 | } 40 | 41 | // Text returns the contents of the text portion of a line. This only really 42 | // makes sense for lines with a :text part, but there are a lot of them. 43 | func (line *Line) Text() string { 44 | if len(line.Args) > 0 { 45 | return line.Args[len(line.Args)-1] 46 | } 47 | return "" 48 | } 49 | 50 | // Target returns the contextual target of the line, usually the first Arg 51 | // for the IRC verb. If the line was broadcast from a channel, the target 52 | // will be that channel. If the line was sent directly by a user, the target 53 | // will be that user. 54 | func (line *Line) Target() string { 55 | // TODO(fluffle): Add 005 CHANTYPES parsing for this? 56 | switch line.Cmd { 57 | case PRIVMSG, NOTICE, ACTION: 58 | if !line.Public() { 59 | return line.Nick 60 | } 61 | case CTCP, CTCPREPLY: 62 | if !line.Public() { 63 | return line.Nick 64 | } 65 | return line.Args[1] 66 | } 67 | if len(line.Args) > 0 { 68 | return line.Args[0] 69 | } 70 | return "" 71 | } 72 | 73 | // Public returns true if the line is the result of an IRC user sending 74 | // a message to a channel the client has joined instead of directly 75 | // to the client. 76 | // 77 | // NOTE: This is very permissive, allowing all 4 RFC channel types even if 78 | // your server doesn't technically support them. 79 | func (line *Line) Public() bool { 80 | switch line.Cmd { 81 | case PRIVMSG, NOTICE, ACTION: 82 | switch line.Args[0][0] { 83 | case '#', '&', '+', '!': 84 | return true 85 | } 86 | case CTCP, CTCPREPLY: 87 | // CTCP prepends the CTCP verb to line.Args, thus for the message 88 | // :nick!user@host PRIVMSG #foo :\001BAR baz\001 89 | // line.Args contains: []string{"BAR", "#foo", "baz"} 90 | // TODO(fluffle): Arguably this is broken, and we should have 91 | // line.Args containing: []string{"#foo", "BAR", "baz"} 92 | // ... OR change conn.Ctcp()'s argument order to be consistent. 93 | switch line.Args[1][0] { 94 | case '#', '&', '+', '!': 95 | return true 96 | } 97 | } 98 | return false 99 | } 100 | 101 | // ParseLine creates a Line from an incoming message from the IRC server. 102 | // 103 | // It contains special casing for CTCP messages, most notably CTCP ACTION. 104 | // All CTCP messages have the \001 bytes stripped from the message and the 105 | // CTCP command separated from any subsequent text. Then, CTCP ACTIONs are 106 | // rewritten such that Line.Cmd == ACTION. Other CTCP messages have Cmd 107 | // set to CTCP or CTCPREPLY, and the CTCP command prepended to line.Args. 108 | // 109 | // ParseLine also parses IRCv3 tags, if received. If a line does not have 110 | // the tags section, Line.Tags will be nil. Tags are optional, and will 111 | // only be included after the correct CAP command. 112 | // 113 | // http://ircv3.net/specs/core/capability-negotiation-3.1.html 114 | // http://ircv3.net/specs/core/message-tags-3.2.html 115 | func ParseLine(s string) *Line { 116 | line := &Line{Raw: s} 117 | 118 | if s == "" { 119 | return nil 120 | } 121 | 122 | if s[0] == '@' { 123 | var rawTags string 124 | line.Tags = make(map[string]string) 125 | if idx := strings.Index(s, " "); idx != -1 { 126 | rawTags, s = s[1:idx], s[idx+1:] 127 | } else { 128 | return nil 129 | } 130 | 131 | // ; is represented as \: in a tag, so it's safe to split on ; 132 | for _, tag := range strings.Split(rawTags, ";") { 133 | if tag == "" { 134 | continue 135 | } 136 | 137 | pair := strings.SplitN(tagsReplacer.Replace(tag), "=", 2) 138 | if len(pair) < 2 { 139 | line.Tags[tag] = "" 140 | } else { 141 | line.Tags[pair[0]] = pair[1] 142 | } 143 | } 144 | } 145 | 146 | if s[0] == ':' { 147 | // remove a source and parse it 148 | if idx := strings.Index(s, " "); idx != -1 { 149 | line.Src, s = s[1:idx], s[idx+1:] 150 | } else { 151 | // pretty sure we shouldn't get here ... 152 | return nil 153 | } 154 | 155 | // src can be the hostname of the irc server or a nick!user@host 156 | line.Host = line.Src 157 | if n, i, h, ok := parseUserHost(line.Src); ok { 158 | line.Nick = n 159 | line.Ident = i 160 | line.Host = h 161 | } 162 | } 163 | 164 | // now we're here, we've parsed a :nick!user@host or :server off 165 | // s should contain "cmd args[] :text" 166 | args := strings.SplitN(s, " :", 2) 167 | if len(args) > 1 { 168 | args = append(strings.Fields(args[0]), args[1]) 169 | } else { 170 | args = strings.Fields(args[0]) 171 | } 172 | line.Cmd = strings.ToUpper(args[0]) 173 | if len(args) > 1 { 174 | line.Args = args[1:] 175 | } 176 | 177 | // So, I think CTCP and (in particular) CTCP ACTION are better handled as 178 | // separate events as opposed to forcing people to have gargantuan 179 | // handlers to cope with the possibilities. 180 | if (line.Cmd == PRIVMSG || line.Cmd == NOTICE) && 181 | len(line.Args[1]) > 2 && 182 | strings.HasPrefix(line.Args[1], "\001") && 183 | strings.HasSuffix(line.Args[1], "\001") { 184 | // WOO, it's a CTCP message 185 | t := strings.SplitN(strings.Trim(line.Args[1], "\001"), " ", 2) 186 | if len(t) > 1 { 187 | // Replace the line with the unwrapped CTCP 188 | line.Args[1] = t[1] 189 | } 190 | if c := strings.ToUpper(t[0]); c == ACTION && line.Cmd == PRIVMSG { 191 | // make a CTCP ACTION it's own event a-la PRIVMSG 192 | line.Cmd = c 193 | } else { 194 | // otherwise, dispatch a generic CTCP/CTCPREPLY event that 195 | // contains the type of CTCP in line.Args[0] 196 | if line.Cmd == PRIVMSG { 197 | line.Cmd = CTCP 198 | } else { 199 | line.Cmd = CTCPREPLY 200 | } 201 | line.Args = append([]string{c}, line.Args...) 202 | } 203 | } 204 | return line 205 | } 206 | 207 | func parseUserHost(uh string) (nick, ident, host string, ok bool) { 208 | uh = strings.TrimSpace(uh) 209 | nidx, uidx := strings.Index(uh, "!"), strings.Index(uh, "@") 210 | if uidx == -1 || nidx == -1 { 211 | return "", "", "", false 212 | } 213 | return uh[:nidx], uh[nidx+1 : uidx], uh[uidx+1:], true 214 | } 215 | 216 | func (line *Line) argslen(minlen int) bool { 217 | pc, _, _, _ := runtime.Caller(1) 218 | fn := runtime.FuncForPC(pc) 219 | if len(line.Args) <= minlen { 220 | logging.Warn("%s: too few arguments: %s", fn.Name(), strings.Join(line.Args, " ")) 221 | return false 222 | } 223 | return true 224 | } 225 | -------------------------------------------------------------------------------- /client/line_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestLineCopy(t *testing.T) { 10 | l1 := &Line{ 11 | Tags: map[string]string{"foo": "bar", "fizz": "buzz"}, 12 | Nick: "nick", 13 | Ident: "ident", 14 | Host: "host", 15 | Src: "src", 16 | Cmd: "cmd", 17 | Raw: "raw", 18 | Args: []string{"arg", "text"}, 19 | Time: time.Now(), 20 | } 21 | 22 | l2 := l1.Copy() 23 | 24 | // Ugly. Couldn't be bothered to bust out reflect and actually think. 25 | if l2.Tags == nil || l2.Tags["foo"] != "bar" || l2.Tags["fizz"] != "buzz" || 26 | l2.Nick != "nick" || l2.Ident != "ident" || l2.Host != "host" || 27 | l2.Src != "src" || l2.Cmd != "cmd" || l2.Raw != "raw" || 28 | l2.Args[0] != "arg" || l2.Args[1] != "text" || l2.Time != l1.Time { 29 | t.Errorf("Line not copied correctly") 30 | t.Errorf("l1: %#v\nl2: %#v", l1, l2) 31 | } 32 | 33 | // Now, modify l2 and verify l1 not changed 34 | l2.Tags["foo"] = "baz" 35 | l2.Nick = l2.Nick[1:] 36 | l2.Ident = "foo" 37 | l2.Host = "" 38 | l2.Args[0] = l2.Args[0][1:] 39 | l2.Args[1] = "bar" 40 | l2.Time = time.Now() 41 | 42 | if l2.Tags == nil || l2.Tags["foo"] != "baz" || l2.Tags["fizz"] != "buzz" || 43 | l1.Nick != "nick" || l1.Ident != "ident" || l1.Host != "host" || 44 | l1.Src != "src" || l1.Cmd != "cmd" || l1.Raw != "raw" || 45 | l1.Args[0] != "arg" || l1.Args[1] != "text" || l1.Time == l2.Time { 46 | t.Errorf("Original modified when copy changed") 47 | t.Errorf("l1: %#v\nl2: %#v", l1, l2) 48 | } 49 | } 50 | 51 | func TestLineText(t *testing.T) { 52 | tests := []struct { 53 | in *Line 54 | out string 55 | }{ 56 | {&Line{}, ""}, 57 | {&Line{Args: []string{"one thing"}}, "one thing"}, 58 | {&Line{Args: []string{"one", "two"}}, "two"}, 59 | } 60 | 61 | for i, test := range tests { 62 | out := test.in.Text() 63 | if out != test.out { 64 | t.Errorf("test %d: expected: '%s', got '%s'", i, test.out, out) 65 | } 66 | } 67 | } 68 | 69 | func TestLineTarget(t *testing.T) { 70 | tests := []struct { 71 | in *Line 72 | out string 73 | }{ 74 | {&Line{}, ""}, 75 | {&Line{Cmd: JOIN, Args: []string{"#foo"}}, "#foo"}, 76 | {&Line{Cmd: PART, Args: []string{"#foo", "bye"}}, "#foo"}, 77 | {&Line{Cmd: PRIVMSG, Args: []string{"Me", "la"}, Nick: "Them"}, "Them"}, 78 | {&Line{Cmd: NOTICE, Args: []string{"Me", "la"}, Nick: "Them"}, "Them"}, 79 | {&Line{Cmd: ACTION, Args: []string{"Me", "la"}, Nick: "Them"}, "Them"}, 80 | {&Line{Cmd: CTCP, Args: []string{"PING", "Me", "1"}, Nick: "Them"}, "Them"}, 81 | {&Line{Cmd: CTCPREPLY, Args: []string{"PONG", "Me", "2"}, Nick: "Them"}, "Them"}, 82 | {&Line{Cmd: PRIVMSG, Args: []string{"#foo", "la"}, Nick: "Them"}, "#foo"}, 83 | {&Line{Cmd: NOTICE, Args: []string{"&foo", "la"}, Nick: "Them"}, "&foo"}, 84 | {&Line{Cmd: ACTION, Args: []string{"!foo", "la"}, Nick: "Them"}, "!foo"}, 85 | {&Line{Cmd: CTCP, Args: []string{"PING", "#foo", "1"}, Nick: "Them"}, "#foo"}, 86 | {&Line{Cmd: CTCPREPLY, Args: []string{"PONG", "#foo", "2"}, Nick: "Them"}, "#foo"}, 87 | } 88 | 89 | for i, test := range tests { 90 | out := test.in.Target() 91 | if out != test.out { 92 | t.Errorf("test %d: expected: '%s', got '%s'", i, test.out, out) 93 | } 94 | } 95 | } 96 | 97 | func TestLineTags(t *testing.T) { 98 | tests := []struct { 99 | in string 100 | out *Line 101 | }{ 102 | { // Make sure ERROR lines work 103 | "ERROR :Closing Link: example.org (Too many user connections (global))", 104 | &Line{ 105 | Nick: "", 106 | Ident: "", 107 | Host: "", 108 | Src: "", 109 | Cmd: ERROR, 110 | Raw: "ERROR :Closing Link: example.org (Too many user connections (global))", 111 | Args: []string{"Closing Link: example.org (Too many user connections (global))"}, 112 | }, 113 | }, 114 | { // Make sure non-tagged lines work 115 | ":nick!ident@host.com PRIVMSG me :Hello", 116 | &Line{ 117 | Nick: "nick", 118 | Ident: "ident", 119 | Host: "host.com", 120 | Src: "nick!ident@host.com", 121 | Cmd: PRIVMSG, 122 | Raw: ":nick!ident@host.com PRIVMSG me :Hello", 123 | Args: []string{"me", "Hello"}, 124 | }, 125 | }, 126 | { // Tags example from the spec 127 | "@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello", 128 | &Line{ 129 | Tags: map[string]string{"aaa": "bbb", "ccc": "", "example.com/ddd": "eee"}, 130 | Nick: "nick", 131 | Ident: "ident", 132 | Host: "host.com", 133 | Src: "nick!ident@host.com", 134 | Cmd: PRIVMSG, 135 | Raw: "@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello", 136 | Args: []string{"me", "Hello"}, 137 | }, 138 | }, 139 | { // Test escaped characters 140 | "@\\:=\\:;\\s=\\s;\\r=\\r;\\n=\\n :nick!ident@host.com PRIVMSG me :Hello", 141 | &Line{ 142 | Tags: map[string]string{";": ";", " ": " ", "\r": "\r", "\n": "\n"}, 143 | Nick: "nick", 144 | Ident: "ident", 145 | Host: "host.com", 146 | Src: "nick!ident@host.com", 147 | Cmd: PRIVMSG, 148 | Raw: "@\\:=\\:;\\s=\\s;\\r=\\r;\\n=\\n :nick!ident@host.com PRIVMSG me :Hello", 149 | Args: []string{"me", "Hello"}, 150 | }, 151 | }, 152 | { // Skip empty tag 153 | "@a=a; :nick!ident@host.com PRIVMSG me :Hello", 154 | &Line{ 155 | Tags: map[string]string{"a": "a"}, 156 | Nick: "nick", 157 | Ident: "ident", 158 | Host: "host.com", 159 | Src: "nick!ident@host.com", 160 | Cmd: PRIVMSG, 161 | Raw: "@a=a; :nick!ident@host.com PRIVMSG me :Hello", 162 | Args: []string{"me", "Hello"}, 163 | }, 164 | }, 165 | { // = in tag 166 | "@a=a=a; :nick!ident@host.com PRIVMSG me :Hello", 167 | &Line{ 168 | Tags: map[string]string{"a": "a=a"}, 169 | Nick: "nick", 170 | Ident: "ident", 171 | Host: "host.com", 172 | Src: "nick!ident@host.com", 173 | Cmd: PRIVMSG, 174 | Raw: "@a=a=a; :nick!ident@host.com PRIVMSG me :Hello", 175 | Args: []string{"me", "Hello"}, 176 | }, 177 | }, 178 | } 179 | 180 | for i, test := range tests { 181 | got := ParseLine(test.in) 182 | if !reflect.DeepEqual(got, test.out) { 183 | t.Errorf("test %d:\nexpected %#v\ngot %#v", i, test.out, got) 184 | } 185 | } 186 | } 187 | 188 | func TestParseUserHost(t *testing.T) { 189 | tests := []struct { 190 | in, nick, ident, host string 191 | ok bool 192 | }{ 193 | {"", "", "", "", false}, 194 | {" ", "", "", "", false}, 195 | {"somestring", "", "", "", false}, 196 | {" s p ", "", "", "", false}, 197 | {"foo!bar", "", "", "", false}, 198 | {"foo@baz.com", "", "", "", false}, 199 | {"foo!bar@baz.com", "foo", "bar", "baz.com", true}, 200 | {" foo!bar@baz.com", "foo", "bar", "baz.com", true}, 201 | {" foo!bar@baz.com ", "foo", "bar", "baz.com", true}, 202 | } 203 | 204 | for i, test := range tests { 205 | nick, ident, host, ok := parseUserHost(test.in) 206 | if test.nick != nick || 207 | test.ident != ident || 208 | test.host != host || 209 | test.ok != ok { 210 | t.Errorf("%d: parseUserHost(%q) = %q, %q, %q, %t; want %q, %q, %q, %t", 211 | i, test.in, nick, ident, host, ok, test.nick, test.ident, test.host, test.ok) 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /client/mocknetconn_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net" 7 | "os" 8 | "strings" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | type mockNetConn struct { 14 | *testing.T 15 | 16 | In, Out chan string 17 | in, out chan []byte 18 | die context.CancelFunc 19 | 20 | closed bool 21 | rt, wt time.Time 22 | } 23 | 24 | func MockNetConn(t *testing.T) *mockNetConn { 25 | // Our mock connection is a testing object 26 | ctx, cancel := context.WithCancel(context.Background()) 27 | m := &mockNetConn{T: t, die: cancel} 28 | 29 | // buffer input 30 | m.In = make(chan string, 20) 31 | m.in = make(chan []byte) 32 | go func() { 33 | for { 34 | select { 35 | case <-ctx.Done(): 36 | return 37 | case s := <-m.In: 38 | m.in <- []byte(s) 39 | } 40 | } 41 | }() 42 | 43 | // buffer output 44 | m.Out = make(chan string) 45 | m.out = make(chan []byte, 20) 46 | go func() { 47 | for { 48 | select { 49 | case <-ctx.Done(): 50 | return 51 | case b := <-m.out: 52 | m.Out <- string(b) 53 | } 54 | } 55 | }() 56 | 57 | return m 58 | } 59 | 60 | // Test helpers 61 | func (m *mockNetConn) Send(s string) { 62 | m.In <- s + "\r\n" 63 | } 64 | 65 | func (m *mockNetConn) Expect(e string) { 66 | select { 67 | case <-time.After(time.Millisecond): 68 | m.Errorf("Mock connection did not receive expected output.\n\t"+ 69 | "Expected: '%s', got nothing.", e) 70 | case s := <-m.Out: 71 | s = strings.Trim(s, "\r\n") 72 | if e != s { 73 | m.Errorf("Mock connection received unexpected value.\n\t"+ 74 | "Expected: '%s'\n\tGot: '%s'", e, s) 75 | } 76 | } 77 | } 78 | 79 | func (m *mockNetConn) ExpectNothing() { 80 | select { 81 | case <-time.After(time.Millisecond): 82 | case s := <-m.Out: 83 | s = strings.Trim(s, "\r\n") 84 | m.Errorf("Mock connection received unexpected output.\n\t"+ 85 | "Expected nothing, got: '%s'", s) 86 | } 87 | } 88 | 89 | // Implement net.Conn interface 90 | func (m *mockNetConn) Read(b []byte) (int, error) { 91 | if m.Closed() { 92 | return 0, os.ErrInvalid 93 | } 94 | s, ok := <-m.in 95 | copy(b, s) 96 | if !ok { 97 | return len(s), io.EOF 98 | } 99 | return len(s), nil 100 | } 101 | 102 | func (m *mockNetConn) Write(s []byte) (int, error) { 103 | if m.Closed() { 104 | return 0, os.ErrInvalid 105 | } 106 | b := make([]byte, len(s)) 107 | copy(b, s) 108 | m.out <- b 109 | return len(s), nil 110 | } 111 | 112 | func (m *mockNetConn) Close() error { 113 | if m.Closed() { 114 | return os.ErrInvalid 115 | } 116 | m.closed = true 117 | // Shut down *ALL* the goroutines! 118 | // This will trigger an EOF event in Read() too 119 | m.die() 120 | close(m.in) 121 | return nil 122 | } 123 | 124 | func (m *mockNetConn) Closed() bool { 125 | return m.closed 126 | } 127 | 128 | func (m *mockNetConn) LocalAddr() net.Addr { 129 | return &net.IPAddr{IP: net.IPv4(127, 0, 0, 1)} 130 | } 131 | 132 | func (m *mockNetConn) RemoteAddr() net.Addr { 133 | return &net.IPAddr{IP: net.IPv4(127, 0, 0, 1)} 134 | } 135 | 136 | func (m *mockNetConn) SetDeadline(t time.Time) error { 137 | m.rt = t 138 | m.wt = t 139 | return nil 140 | } 141 | 142 | func (m *mockNetConn) SetReadDeadline(t time.Time) error { 143 | m.rt = t 144 | return nil 145 | } 146 | 147 | func (m *mockNetConn) SetWriteDeadline(t time.Time) error { 148 | m.wt = t 149 | return nil 150 | } 151 | -------------------------------------------------------------------------------- /client/sasl_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/emersion/go-sasl" 5 | "testing" 6 | ) 7 | 8 | func TestSaslPlainSuccessWorkflow(t *testing.T) { 9 | c, s := setUp(t) 10 | defer s.tearDown() 11 | 12 | c.Config().Sasl = sasl.NewPlainClient("", "example", "password") 13 | c.Config().EnableCapabilityNegotiation = true 14 | 15 | c.h_REGISTER(&Line{Cmd: REGISTER}) 16 | s.nc.Expect("CAP LS") 17 | s.nc.Expect("NICK test") 18 | s.nc.Expect("USER test 12 * :Testing IRC") 19 | s.nc.Send("CAP * LS :sasl foobar") 20 | s.nc.Expect("CAP REQ :sasl") 21 | s.nc.Send("CAP * ACK :sasl") 22 | s.nc.Expect("AUTHENTICATE PLAIN") 23 | s.nc.Send("AUTHENTICATE +") 24 | s.nc.Expect("AUTHENTICATE AGV4YW1wbGUAcGFzc3dvcmQ=") 25 | s.nc.Send("904 test :SASL authentication successful") 26 | s.nc.Expect("CAP END") 27 | } 28 | 29 | func TestSaslPlainWrongPassword(t *testing.T) { 30 | c, s := setUp(t) 31 | defer s.tearDown() 32 | 33 | c.Config().Sasl = sasl.NewPlainClient("", "example", "password") 34 | c.Config().EnableCapabilityNegotiation = true 35 | 36 | c.h_REGISTER(&Line{Cmd: REGISTER}) 37 | s.nc.Expect("CAP LS") 38 | s.nc.Expect("NICK test") 39 | s.nc.Expect("USER test 12 * :Testing IRC") 40 | s.nc.Send("CAP * LS :sasl foobar") 41 | s.nc.Expect("CAP REQ :sasl") 42 | s.nc.Send("CAP * ACK :sasl") 43 | s.nc.Expect("AUTHENTICATE PLAIN") 44 | s.nc.Send("AUTHENTICATE +") 45 | s.nc.Expect("AUTHENTICATE AGV4YW1wbGUAcGFzc3dvcmQ=") 46 | s.nc.Send("904 test :SASL authentication failed") 47 | s.nc.Expect("CAP END") 48 | } 49 | 50 | func TestSaslExternalSuccessWorkflow(t *testing.T) { 51 | c, s := setUp(t) 52 | defer s.tearDown() 53 | 54 | c.Config().Sasl = sasl.NewExternalClient("") 55 | c.Config().EnableCapabilityNegotiation = true 56 | 57 | c.h_REGISTER(&Line{Cmd: REGISTER}) 58 | s.nc.Expect("CAP LS") 59 | s.nc.Expect("NICK test") 60 | s.nc.Expect("USER test 12 * :Testing IRC") 61 | s.nc.Send("CAP * LS :sasl foobar") 62 | s.nc.Expect("CAP REQ :sasl") 63 | s.nc.Send("CAP * ACK :sasl") 64 | s.nc.Expect("AUTHENTICATE EXTERNAL") 65 | s.nc.Send("AUTHENTICATE +") 66 | s.nc.Expect("AUTHENTICATE +") 67 | s.nc.Send("904 test :SASL authentication successful") 68 | s.nc.Expect("CAP END") 69 | } 70 | 71 | func TestSaslNoSaslCap(t *testing.T) { 72 | c, s := setUp(t) 73 | defer s.tearDown() 74 | 75 | c.Config().Sasl = sasl.NewPlainClient("", "example", "password") 76 | c.Config().EnableCapabilityNegotiation = true 77 | 78 | c.h_REGISTER(&Line{Cmd: REGISTER}) 79 | s.nc.Expect("CAP LS") 80 | s.nc.Expect("NICK test") 81 | s.nc.Expect("USER test 12 * :Testing IRC") 82 | s.nc.Send("CAP * LS :foobar") 83 | s.nc.Expect("CAP END") 84 | } 85 | 86 | func TestSaslUnsupportedMechanism(t *testing.T) { 87 | c, s := setUp(t) 88 | defer s.tearDown() 89 | 90 | c.Config().Sasl = sasl.NewPlainClient("", "example", "password") 91 | c.Config().EnableCapabilityNegotiation = true 92 | 93 | c.h_REGISTER(&Line{Cmd: REGISTER}) 94 | s.nc.Expect("CAP LS") 95 | s.nc.Expect("NICK test") 96 | s.nc.Expect("USER test 12 * :Testing IRC") 97 | s.nc.Send("CAP * LS :sasl foobar") 98 | s.nc.Expect("CAP REQ :sasl") 99 | s.nc.Send("CAP * ACK :sasl") 100 | s.nc.Expect("AUTHENTICATE PLAIN") 101 | s.nc.Send("908 test external :are available SASL mechanisms") 102 | s.nc.Expect("CAP END") 103 | } 104 | -------------------------------------------------------------------------------- /client/state_handlers.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | // this file contains the extra set of event handlers 4 | // to manage tracking state for an IRC connection 5 | 6 | import ( 7 | "strings" 8 | 9 | "github.com/fluffle/goirc/logging" 10 | ) 11 | 12 | var stHandlers = map[string]HandlerFunc{ 13 | "JOIN": (*Conn).h_JOIN, 14 | "KICK": (*Conn).h_KICK, 15 | "MODE": (*Conn).h_MODE, 16 | "NICK": (*Conn).h_STNICK, 17 | "PART": (*Conn).h_PART, 18 | "QUIT": (*Conn).h_QUIT, 19 | "TOPIC": (*Conn).h_TOPIC, 20 | "311": (*Conn).h_311, 21 | "324": (*Conn).h_324, 22 | "332": (*Conn).h_332, 23 | "352": (*Conn).h_352, 24 | "353": (*Conn).h_353, 25 | "671": (*Conn).h_671, 26 | } 27 | 28 | func (conn *Conn) addSTHandlers() { 29 | for n, h := range stHandlers { 30 | conn.stRemovers = append(conn.stRemovers, conn.handle(n, h)) 31 | } 32 | } 33 | 34 | func (conn *Conn) delSTHandlers() { 35 | for _, h := range conn.stRemovers { 36 | h.Remove() 37 | } 38 | conn.stRemovers = conn.stRemovers[:0] 39 | } 40 | 41 | // Handle NICK messages that need to update the state tracker 42 | func (conn *Conn) h_STNICK(line *Line) { 43 | // all nicks should be handled the same way, our own included 44 | conn.st.ReNick(line.Nick, line.Args[0]) 45 | } 46 | 47 | // Handle JOINs to channels to maintain state 48 | func (conn *Conn) h_JOIN(line *Line) { 49 | ch := conn.st.GetChannel(line.Args[0]) 50 | nk := conn.st.GetNick(line.Nick) 51 | if ch == nil { 52 | // first we've seen of this channel, so should be us joining it 53 | // NOTE this will also take care of nk == nil && ch == nil 54 | if !conn.Me().Equals(nk) { 55 | logging.Warn("irc.JOIN(): JOIN to unknown channel %s received "+ 56 | "from (non-me) nick %s", line.Args[0], line.Nick) 57 | return 58 | } 59 | conn.st.NewChannel(line.Args[0]) 60 | // since we don't know much about this channel, ask server for info 61 | // we get the channel users automatically in 353 and the channel 62 | // topic in 332 on join, so we just need to get the modes 63 | conn.Mode(line.Args[0]) 64 | // sending a WHO for the channel is MUCH more efficient than 65 | // triggering a WHOIS on every nick from the 353 handler 66 | conn.Who(line.Args[0]) 67 | } 68 | if nk == nil { 69 | // this is the first we've seen of this nick 70 | conn.st.NewNick(line.Nick) 71 | conn.st.NickInfo(line.Nick, line.Ident, line.Host, "") 72 | // since we don't know much about this nick, ask server for info 73 | conn.Who(line.Nick) 74 | } 75 | // this takes care of both nick and channel linking \o/ 76 | conn.st.Associate(line.Args[0], line.Nick) 77 | } 78 | 79 | // Handle PARTs from channels to maintain state 80 | func (conn *Conn) h_PART(line *Line) { 81 | conn.st.Dissociate(line.Args[0], line.Nick) 82 | } 83 | 84 | // Handle KICKs from channels to maintain state 85 | func (conn *Conn) h_KICK(line *Line) { 86 | if !line.argslen(1) { 87 | return 88 | } 89 | // XXX: this won't handle autorejoining channels on KICK 90 | // it's trivial to do this in a seperate handler... 91 | conn.st.Dissociate(line.Args[0], line.Args[1]) 92 | } 93 | 94 | // Handle other people's QUITs 95 | func (conn *Conn) h_QUIT(line *Line) { 96 | conn.st.DelNick(line.Nick) 97 | } 98 | 99 | // Handle MODE changes for channels we know about (and our nick personally) 100 | func (conn *Conn) h_MODE(line *Line) { 101 | if !line.argslen(1) { 102 | return 103 | } 104 | if ch := conn.st.GetChannel(line.Args[0]); ch != nil { 105 | // channel modes first 106 | conn.st.ChannelModes(line.Args[0], line.Args[1], line.Args[2:]...) 107 | } else if nk := conn.st.GetNick(line.Args[0]); nk != nil { 108 | // nick mode change, should be us 109 | if !conn.Me().Equals(nk) { 110 | logging.Warn("irc.MODE(): recieved MODE %s for (non-me) nick %s", 111 | line.Args[1], line.Args[0]) 112 | return 113 | } 114 | conn.st.NickModes(line.Args[0], line.Args[1]) 115 | } else { 116 | logging.Warn("irc.MODE(): not sure what to do with MODE %s", 117 | strings.Join(line.Args, " ")) 118 | } 119 | } 120 | 121 | // Handle TOPIC changes for channels 122 | func (conn *Conn) h_TOPIC(line *Line) { 123 | if !line.argslen(1) { 124 | return 125 | } 126 | if ch := conn.st.GetChannel(line.Args[0]); ch != nil { 127 | conn.st.Topic(line.Args[0], line.Args[1]) 128 | } else { 129 | logging.Warn("irc.TOPIC(): topic change on unknown channel %s", 130 | line.Args[0]) 131 | } 132 | } 133 | 134 | // Handle 311 whois reply 135 | func (conn *Conn) h_311(line *Line) { 136 | if !line.argslen(5) { 137 | return 138 | } 139 | if nk := conn.st.GetNick(line.Args[1]); (nk != nil) && !conn.Me().Equals(nk) { 140 | conn.st.NickInfo(line.Args[1], line.Args[2], line.Args[3], line.Args[5]) 141 | } else { 142 | logging.Warn("irc.311(): received WHOIS info for unknown nick %s", 143 | line.Args[1]) 144 | } 145 | } 146 | 147 | // Handle 324 mode reply 148 | func (conn *Conn) h_324(line *Line) { 149 | if !line.argslen(2) { 150 | return 151 | } 152 | if ch := conn.st.GetChannel(line.Args[1]); ch != nil { 153 | conn.st.ChannelModes(line.Args[1], line.Args[2], line.Args[3:]...) 154 | } else { 155 | logging.Warn("irc.324(): received MODE settings for unknown channel %s", 156 | line.Args[1]) 157 | } 158 | } 159 | 160 | // Handle 332 topic reply on join to channel 161 | func (conn *Conn) h_332(line *Line) { 162 | if !line.argslen(2) { 163 | return 164 | } 165 | if ch := conn.st.GetChannel(line.Args[1]); ch != nil { 166 | conn.st.Topic(line.Args[1], line.Args[2]) 167 | } else { 168 | logging.Warn("irc.332(): received TOPIC value for unknown channel %s", 169 | line.Args[1]) 170 | } 171 | } 172 | 173 | // Handle 352 who reply 174 | func (conn *Conn) h_352(line *Line) { 175 | if !line.argslen(5) { 176 | return 177 | } 178 | nk := conn.st.GetNick(line.Args[5]) 179 | if nk == nil { 180 | logging.Warn("irc.352(): received WHO reply for unknown nick %s", 181 | line.Args[5]) 182 | return 183 | } 184 | if conn.Me().Equals(nk) { 185 | return 186 | } 187 | // XXX: do we care about the actual server the nick is on? 188 | // or the hop count to this server? 189 | // last arg contains " " 190 | a := strings.SplitN(line.Args[len(line.Args)-1], " ", 2) 191 | conn.st.NickInfo(nk.Nick, line.Args[2], line.Args[3], a[1]) 192 | if !line.argslen(6) { 193 | return 194 | } 195 | if idx := strings.Index(line.Args[6], "*"); idx != -1 { 196 | conn.st.NickModes(nk.Nick, "+o") 197 | } 198 | if idx := strings.Index(line.Args[6], "B"); idx != -1 { 199 | conn.st.NickModes(nk.Nick, "+B") 200 | } 201 | if idx := strings.Index(line.Args[6], "H"); idx != -1 { 202 | conn.st.NickModes(nk.Nick, "+i") 203 | } 204 | } 205 | 206 | // Handle 353 names reply 207 | func (conn *Conn) h_353(line *Line) { 208 | if !line.argslen(2) { 209 | return 210 | } 211 | if ch := conn.st.GetChannel(line.Args[2]); ch != nil { 212 | nicks := strings.Split(line.Args[len(line.Args)-1], " ") 213 | for _, nick := range nicks { 214 | // UnrealIRCd's coders are lazy and leave a trailing space 215 | if nick == "" { 216 | continue 217 | } 218 | switch c := nick[0]; c { 219 | case '~', '&', '@', '%', '+': 220 | nick = nick[1:] 221 | fallthrough 222 | default: 223 | if conn.st.GetNick(nick) == nil { 224 | // we don't know this nick yet! 225 | conn.st.NewNick(nick) 226 | } 227 | if _, ok := conn.st.IsOn(ch.Name, nick); !ok { 228 | // This nick isn't associated with this channel yet! 229 | conn.st.Associate(ch.Name, nick) 230 | } 231 | switch c { 232 | case '~': 233 | conn.st.ChannelModes(ch.Name, "+q", nick) 234 | case '&': 235 | conn.st.ChannelModes(ch.Name, "+a", nick) 236 | case '@': 237 | conn.st.ChannelModes(ch.Name, "+o", nick) 238 | case '%': 239 | conn.st.ChannelModes(ch.Name, "+h", nick) 240 | case '+': 241 | conn.st.ChannelModes(ch.Name, "+v", nick) 242 | } 243 | } 244 | } 245 | } else { 246 | logging.Warn("irc.353(): received NAMES list for unknown channel %s", 247 | line.Args[2]) 248 | } 249 | } 250 | 251 | // Handle 671 whois reply (nick connected via SSL) 252 | func (conn *Conn) h_671(line *Line) { 253 | if !line.argslen(1) { 254 | return 255 | } 256 | if nk := conn.st.GetNick(line.Args[1]); nk != nil { 257 | conn.st.NickModes(nk.Nick, "+z") 258 | } else { 259 | logging.Warn("irc.671(): received WHOIS SSL info for unknown nick %s", 260 | line.Args[1]) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fluffle/goirc 2 | 3 | require ( 4 | github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead 5 | github.com/fluffle/golog/logging v0.0.0-20180928190033-7d99e85061cb 6 | github.com/golang/glog v1.0.0 7 | github.com/golang/mock v1.5.0 8 | golang.org/x/net v0.18.0 9 | ) 10 | 11 | go 1.13 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y= 2 | github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= 3 | github.com/fluffle/golog/logging v0.0.0-20180928190033-7d99e85061cb h1:r//eMD/5sdzxVy34UP5fFvRWIL2L8QZtaDgVCKfVQLI= 4 | github.com/fluffle/golog/logging v0.0.0-20180928190033-7d99e85061cb/go.mod h1:w8+az2+kPHMcsaKnTnGapWTNToJK8BogkHiAncvqKsM= 5 | github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= 6 | github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= 7 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 8 | github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= 9 | github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= 10 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 11 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 12 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 13 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 14 | golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= 15 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 16 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 17 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 18 | golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 19 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 20 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 21 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 22 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 23 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 24 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= 25 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 26 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 27 | golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= 28 | golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= 29 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 30 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 31 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 32 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 33 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 34 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 41 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 42 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 43 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 44 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 45 | golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= 46 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 47 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 48 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 49 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 50 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 51 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 52 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 53 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 54 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 55 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 56 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 57 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 58 | -------------------------------------------------------------------------------- /logging/glog/glog.go: -------------------------------------------------------------------------------- 1 | package glog 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fluffle/goirc/logging" 6 | "github.com/golang/glog" 7 | ) 8 | 9 | // Simple adapter to utilise Google's GLog package with goirc. 10 | // Just import this package alongside goirc/client and call 11 | // glog.Init() in your main() to set things up. 12 | type GLogger struct{} 13 | 14 | func (gl GLogger) Debug(f string, a ...interface{}) { 15 | // GLog doesn't have a "Debug" level, so use V(2) instead. 16 | if glog.V(2) { 17 | glog.InfoDepth(3, fmt.Sprintf(f, a...)) 18 | } 19 | } 20 | func (gl GLogger) Info(f string, a ...interface{}) { 21 | glog.InfoDepth(3, fmt.Sprintf(f, a...)) 22 | } 23 | func (gl GLogger) Warn(f string, a ...interface{}) { 24 | glog.WarningDepth(3, fmt.Sprintf(f, a...)) 25 | } 26 | func (gl GLogger) Error(f string, a ...interface{}) { 27 | glog.ErrorDepth(3, fmt.Sprintf(f, a...)) 28 | } 29 | 30 | func Init() { 31 | logging.SetLogger(GLogger{}) 32 | } 33 | -------------------------------------------------------------------------------- /logging/golog/golog.go: -------------------------------------------------------------------------------- 1 | package golog 2 | 3 | import ( 4 | "github.com/fluffle/goirc/logging" 5 | log "github.com/fluffle/golog/logging" 6 | ) 7 | 8 | // Simple adapter to utilise my logging package with goirc. 9 | // Just import this package alongside goirc/client and call 10 | // golog.Init() in your main() to set things up. 11 | func Init() { 12 | l := log.NewFromFlags() 13 | l.SetDepth(1) 14 | logging.SetLogger(l) 15 | } 16 | -------------------------------------------------------------------------------- /logging/logging.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | // The IRC client will log things using these methods 4 | type Logger interface { 5 | // Debug logging of raw socket comms to/from server. 6 | Debug(format string, args ...interface{}) 7 | // Informational logging about client behaviour. 8 | Info(format string, args ...interface{}) 9 | // Warnings of inconsistent or unexpected data, mostly 10 | // related to state tracking of IRC nicks/chans. 11 | Warn(format string, args ...interface{}) 12 | // Errors, mostly to do with network communication. 13 | Error(format string, args ...interface{}) 14 | } 15 | 16 | // By default we do no logging. Logging is enabled or disabled 17 | // at the package level, since I'm lazy and re-re-reorganising 18 | // my code to pass a per-client-struct Logger around to all the 19 | // state objects is a pain in the arse. 20 | var logger Logger = nullLogger{} 21 | 22 | // SetLogger sets the internal goirc Logger to l. If l is nil, 23 | // a dummy logger that does nothing is installed instead. 24 | func SetLogger(l Logger) { 25 | if l == nil { 26 | logger = nullLogger{} 27 | } else { 28 | logger = l 29 | } 30 | } 31 | 32 | // A nullLogger does nothing while fulfilling Logger. 33 | type nullLogger struct{} 34 | 35 | func (nl nullLogger) Debug(f string, a ...interface{}) {} 36 | func (nl nullLogger) Info(f string, a ...interface{}) {} 37 | func (nl nullLogger) Warn(f string, a ...interface{}) {} 38 | func (nl nullLogger) Error(f string, a ...interface{}) {} 39 | 40 | // Shim functions so that the package can be used directly 41 | func Debug(f string, a ...interface{}) { logger.Debug(f, a...) } 42 | func Info(f string, a ...interface{}) { logger.Info(f, a...) } 43 | func Warn(f string, a ...interface{}) { logger.Warn(f, a...) } 44 | func Error(f string, a ...interface{}) { logger.Error(f, a...) } 45 | -------------------------------------------------------------------------------- /state/channel.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "github.com/fluffle/goirc/logging" 5 | 6 | "reflect" 7 | "strconv" 8 | ) 9 | 10 | // A Channel is returned from the state tracker and contains 11 | // a copy of the channel state at a particular time. 12 | type Channel struct { 13 | Name, Topic string 14 | Modes *ChanMode 15 | Nicks map[string]*ChanPrivs 16 | } 17 | 18 | // Internal bookkeeping struct for channels. 19 | type channel struct { 20 | name, topic string 21 | modes *ChanMode 22 | lookup map[string]*nick 23 | nicks map[*nick]*ChanPrivs 24 | } 25 | 26 | // A struct representing the modes of an IRC Channel 27 | // (the ones we care about, at least). 28 | // http://www.unrealircd.com/files/docs/unreal32docs.html#userchannelmodes 29 | type ChanMode struct { 30 | // MODE +p, +s, +t, +n, +m 31 | Private, Secret, ProtectedTopic, NoExternalMsg, Moderated bool 32 | 33 | // MODE +i, +O, +z 34 | InviteOnly, OperOnly, SSLOnly bool 35 | 36 | // MODE +r, +Z 37 | Registered, AllSSL bool 38 | 39 | // MODE +k 40 | Key string 41 | 42 | // MODE +l 43 | Limit int 44 | } 45 | 46 | // A struct representing the modes a Nick can have on a Channel 47 | type ChanPrivs struct { 48 | // MODE +q, +a, +o, +h, +v 49 | Owner, Admin, Op, HalfOp, Voice bool 50 | } 51 | 52 | // Map ChanMode fields to IRC mode characters 53 | var StringToChanMode = map[string]string{} 54 | var ChanModeToString = map[string]string{ 55 | "Private": "p", 56 | "Secret": "s", 57 | "ProtectedTopic": "t", 58 | "NoExternalMsg": "n", 59 | "Moderated": "m", 60 | "InviteOnly": "i", 61 | "OperOnly": "O", 62 | "SSLOnly": "z", 63 | "Registered": "r", 64 | "AllSSL": "Z", 65 | "Key": "k", 66 | "Limit": "l", 67 | } 68 | 69 | // Map *irc.ChanPrivs fields to IRC mode characters 70 | var StringToChanPriv = map[string]string{} 71 | var ChanPrivToString = map[string]string{ 72 | "Owner": "q", 73 | "Admin": "a", 74 | "Op": "o", 75 | "HalfOp": "h", 76 | "Voice": "v", 77 | } 78 | 79 | // Map *irc.ChanPrivs fields to the symbols used to represent these modes 80 | // in NAMES and WHOIS responses 81 | var ModeCharToChanPriv = map[byte]string{} 82 | var ChanPrivToModeChar = map[string]byte{ 83 | "Owner": '~', 84 | "Admin": '&', 85 | "Op": '@', 86 | "HalfOp": '%', 87 | "Voice": '+', 88 | } 89 | 90 | // Init function to fill in reverse mappings for *toString constants. 91 | func init() { 92 | for k, v := range ChanModeToString { 93 | StringToChanMode[v] = k 94 | } 95 | for k, v := range ChanPrivToString { 96 | StringToChanPriv[v] = k 97 | } 98 | for k, v := range ChanPrivToModeChar { 99 | ModeCharToChanPriv[v] = k 100 | } 101 | } 102 | 103 | /******************************************************************************\ 104 | * Channel methods for state management 105 | \******************************************************************************/ 106 | 107 | func newChannel(name string) *channel { 108 | return &channel{ 109 | name: name, 110 | modes: new(ChanMode), 111 | nicks: make(map[*nick]*ChanPrivs), 112 | lookup: make(map[string]*nick), 113 | } 114 | } 115 | 116 | // Returns a copy of the internal tracker channel state at this time. 117 | // Relies on tracker-level locking for concurrent access. 118 | func (ch *channel) Channel() *Channel { 119 | c := &Channel{ 120 | Name: ch.name, 121 | Topic: ch.topic, 122 | Modes: ch.modes.Copy(), 123 | Nicks: make(map[string]*ChanPrivs), 124 | } 125 | for n, cp := range ch.nicks { 126 | c.Nicks[n.nick] = cp.Copy() 127 | } 128 | return c 129 | } 130 | 131 | func (ch *channel) isOn(nk *nick) (*ChanPrivs, bool) { 132 | cp, ok := ch.nicks[nk] 133 | return cp.Copy(), ok 134 | } 135 | 136 | // Associates a Nick with a Channel 137 | func (ch *channel) addNick(nk *nick, cp *ChanPrivs) { 138 | if _, ok := ch.nicks[nk]; !ok { 139 | ch.nicks[nk] = cp 140 | ch.lookup[nk.nick] = nk 141 | } else { 142 | logging.Warn("Channel.addNick(): %s already on %s.", nk.nick, ch.name) 143 | } 144 | } 145 | 146 | // Disassociates a Nick from a Channel. 147 | func (ch *channel) delNick(nk *nick) { 148 | if _, ok := ch.nicks[nk]; ok { 149 | delete(ch.nicks, nk) 150 | delete(ch.lookup, nk.nick) 151 | } else { 152 | logging.Warn("Channel.delNick(): %s not on %s.", nk.nick, ch.name) 153 | } 154 | } 155 | 156 | // Parses mode strings for a channel. 157 | func (ch *channel) parseModes(modes string, modeargs ...string) { 158 | var modeop bool // true => add mode, false => remove mode 159 | var modestr string 160 | for i := 0; i < len(modes); i++ { 161 | switch m := modes[i]; m { 162 | case '+': 163 | modeop = true 164 | modestr = string(m) 165 | case '-': 166 | modeop = false 167 | modestr = string(m) 168 | case 'i': 169 | ch.modes.InviteOnly = modeop 170 | case 'm': 171 | ch.modes.Moderated = modeop 172 | case 'n': 173 | ch.modes.NoExternalMsg = modeop 174 | case 'p': 175 | ch.modes.Private = modeop 176 | case 'r': 177 | ch.modes.Registered = modeop 178 | case 's': 179 | ch.modes.Secret = modeop 180 | case 't': 181 | ch.modes.ProtectedTopic = modeop 182 | case 'z': 183 | ch.modes.SSLOnly = modeop 184 | case 'Z': 185 | ch.modes.AllSSL = modeop 186 | case 'O': 187 | ch.modes.OperOnly = modeop 188 | case 'k': 189 | if modeop && len(modeargs) != 0 { 190 | ch.modes.Key, modeargs = modeargs[0], modeargs[1:] 191 | } else if !modeop { 192 | ch.modes.Key = "" 193 | } else { 194 | logging.Warn("Channel.ParseModes(): not enough arguments to "+ 195 | "process MODE %s %s%c", ch.name, modestr, m) 196 | } 197 | case 'l': 198 | if modeop && len(modeargs) != 0 { 199 | ch.modes.Limit, _ = strconv.Atoi(modeargs[0]) 200 | modeargs = modeargs[1:] 201 | } else if !modeop { 202 | ch.modes.Limit = 0 203 | } else { 204 | logging.Warn("Channel.ParseModes(): not enough arguments to "+ 205 | "process MODE %s %s%c", ch.name, modestr, m) 206 | } 207 | case 'q', 'a', 'o', 'h', 'v': 208 | if len(modeargs) != 0 { 209 | if nk, ok := ch.lookup[modeargs[0]]; ok { 210 | cp := ch.nicks[nk] 211 | switch m { 212 | case 'q': 213 | cp.Owner = modeop 214 | case 'a': 215 | cp.Admin = modeop 216 | case 'o': 217 | cp.Op = modeop 218 | case 'h': 219 | cp.HalfOp = modeop 220 | case 'v': 221 | cp.Voice = modeop 222 | } 223 | modeargs = modeargs[1:] 224 | } else { 225 | logging.Warn("Channel.ParseModes(): untracked nick %s "+ 226 | "received MODE on channel %s", modeargs[0], ch.name) 227 | } 228 | } else { 229 | logging.Warn("Channel.ParseModes(): not enough arguments to "+ 230 | "process MODE %s %s%c", ch.name, modestr, m) 231 | } 232 | default: 233 | logging.Info("Channel.ParseModes(): unknown mode char %c", m) 234 | } 235 | } 236 | } 237 | 238 | // Returns true if the Nick is associated with the Channel 239 | func (ch *Channel) IsOn(nk string) (*ChanPrivs, bool) { 240 | cp, ok := ch.Nicks[nk] 241 | return cp, ok 242 | } 243 | 244 | // Test Channel equality. 245 | func (ch *Channel) Equals(other *Channel) bool { 246 | return reflect.DeepEqual(ch, other) 247 | } 248 | 249 | // Duplicates a ChanMode struct. 250 | func (cm *ChanMode) Copy() *ChanMode { 251 | if cm == nil { 252 | return nil 253 | } 254 | c := *cm 255 | return &c 256 | } 257 | 258 | // Test ChanMode equality. 259 | func (cm *ChanMode) Equals(other *ChanMode) bool { 260 | return reflect.DeepEqual(cm, other) 261 | } 262 | 263 | // Duplicates a ChanPrivs struct. 264 | func (cp *ChanPrivs) Copy() *ChanPrivs { 265 | if cp == nil { 266 | return nil 267 | } 268 | c := *cp 269 | return &c 270 | } 271 | 272 | // Test ChanPrivs equality. 273 | func (cp *ChanPrivs) Equals(other *ChanPrivs) bool { 274 | return reflect.DeepEqual(cp, other) 275 | } 276 | 277 | // Returns a string representing the channel. Looks like: 278 | // Channel: e.g. #moo 279 | // Topic: e.g. Discussing the merits of cows! 280 | // Mode: e.g. +nsti 281 | // Nicks: 282 | // : e.g. CowMaster: +o 283 | // ... 284 | func (ch *Channel) String() string { 285 | str := "Channel: " + ch.Name + "\n\t" 286 | str += "Topic: " + ch.Topic + "\n\t" 287 | str += "Modes: " + ch.Modes.String() + "\n\t" 288 | str += "Nicks: \n" 289 | for nk, cp := range ch.Nicks { 290 | str += "\t\t" + nk + ": " + cp.String() + "\n" 291 | } 292 | return str 293 | } 294 | 295 | func (ch *channel) String() string { 296 | return ch.Channel().String() 297 | } 298 | 299 | // Returns a string representing the channel modes. Looks like: 300 | // +npk key 301 | func (cm *ChanMode) String() string { 302 | if cm == nil { 303 | return "No modes set" 304 | } 305 | str := "+" 306 | a := make([]string, 0) 307 | v := reflect.Indirect(reflect.ValueOf(cm)) 308 | t := v.Type() 309 | for i := 0; i < v.NumField(); i++ { 310 | switch f := v.Field(i); f.Kind() { 311 | case reflect.Bool: 312 | if f.Bool() { 313 | str += ChanModeToString[t.Field(i).Name] 314 | } 315 | case reflect.String: 316 | if f.String() != "" { 317 | str += ChanModeToString[t.Field(i).Name] 318 | a = append(a, f.String()) 319 | } 320 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 321 | if f.Int() != 0 { 322 | str += ChanModeToString[t.Field(i).Name] 323 | a = append(a, strconv.FormatInt(f.Int(), 10)) 324 | } 325 | } 326 | } 327 | for _, s := range a { 328 | if s != "" { 329 | str += " " + s 330 | } 331 | } 332 | if str == "+" { 333 | str = "No modes set" 334 | } 335 | return str 336 | } 337 | 338 | // Returns a string representing the channel privileges. Looks like: 339 | // +o 340 | func (cp *ChanPrivs) String() string { 341 | if cp == nil { 342 | return "No modes set" 343 | } 344 | str := "+" 345 | v := reflect.Indirect(reflect.ValueOf(cp)) 346 | t := v.Type() 347 | for i := 0; i < v.NumField(); i++ { 348 | switch f := v.Field(i); f.Kind() { 349 | // only bools here at the mo too! 350 | case reflect.Bool: 351 | if f.Bool() { 352 | str += ChanPrivToString[t.Field(i).Name] 353 | } 354 | } 355 | } 356 | if str == "+" { 357 | str = "No modes set" 358 | } 359 | return str 360 | } 361 | -------------------------------------------------------------------------------- /state/channel_test.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import "testing" 4 | 5 | func compareChannel(t *testing.T, ch *channel) { 6 | c := ch.Channel() 7 | if c.Name != ch.name || c.Topic != ch.topic || 8 | !c.Modes.Equals(ch.modes) || len(c.Nicks) != len(ch.nicks) { 9 | t.Errorf("Channel not duped correctly from internal state.") 10 | } 11 | for nk, cp := range ch.nicks { 12 | if other, ok := c.Nicks[nk.nick]; !ok || !cp.Equals(other) { 13 | t.Errorf("Nick not duped correctly from internal state.") 14 | } 15 | } 16 | } 17 | 18 | func TestNewChannel(t *testing.T) { 19 | ch := newChannel("#test1") 20 | 21 | if ch.name != "#test1" { 22 | t.Errorf("Channel not created correctly by NewChannel()") 23 | } 24 | if len(ch.nicks) != 0 || len(ch.lookup) != 0 { 25 | t.Errorf("Channel maps contain data after NewChannel()") 26 | } 27 | compareChannel(t, ch) 28 | } 29 | 30 | func TestAddNick(t *testing.T) { 31 | ch := newChannel("#test1") 32 | nk := newNick("test1") 33 | cp := new(ChanPrivs) 34 | 35 | ch.addNick(nk, cp) 36 | 37 | if len(ch.nicks) != 1 || len(ch.lookup) != 1 { 38 | t.Errorf("Nick lists not updated correctly for add.") 39 | } 40 | if c, ok := ch.nicks[nk]; !ok || c != cp { 41 | t.Errorf("Nick test1 not properly stored in nicks map.") 42 | } 43 | if n, ok := ch.lookup["test1"]; !ok || n != nk { 44 | t.Errorf("Nick test1 not properly stored in lookup map.") 45 | } 46 | compareChannel(t, ch) 47 | } 48 | 49 | func TestDelNick(t *testing.T) { 50 | ch := newChannel("#test1") 51 | nk := newNick("test1") 52 | cp := new(ChanPrivs) 53 | 54 | ch.addNick(nk, cp) 55 | ch.delNick(nk) 56 | if len(ch.nicks) != 0 || len(ch.lookup) != 0 { 57 | t.Errorf("Nick lists not updated correctly for del.") 58 | } 59 | if c, ok := ch.nicks[nk]; ok || c != nil { 60 | t.Errorf("Nick test1 not properly removed from nicks map.") 61 | } 62 | if n, ok := ch.lookup["#test1"]; ok || n != nil { 63 | t.Errorf("Nick test1 not properly removed from lookup map.") 64 | } 65 | compareChannel(t, ch) 66 | } 67 | 68 | func TestChannelParseModes(t *testing.T) { 69 | ch := newChannel("#test1") 70 | md := ch.modes 71 | 72 | // Channel modes can adjust channel privs too, so we need a Nick 73 | nk := newNick("test1") 74 | cp := new(ChanPrivs) 75 | ch.addNick(nk, cp) 76 | 77 | // Test bools first. 78 | compareChannel(t, ch) 79 | if md.Private || md.Secret || md.ProtectedTopic || md.NoExternalMsg || 80 | md.Moderated || md.InviteOnly || md.OperOnly || md.SSLOnly { 81 | t.Errorf("Modes for new channel set to true.") 82 | } 83 | 84 | // Flip some bits! 85 | md.Private = true 86 | md.NoExternalMsg = true 87 | md.InviteOnly = true 88 | 89 | // Flip some MOAR bits. 90 | ch.parseModes("+s-p+tm-i") 91 | 92 | compareChannel(t, ch) 93 | if md.Private || !md.Secret || !md.ProtectedTopic || !md.NoExternalMsg || 94 | !md.Moderated || md.InviteOnly || md.OperOnly || md.SSLOnly { 95 | t.Errorf("Modes not flipped correctly by ParseModes.") 96 | } 97 | 98 | // Test numeric parsing (currently only channel limits) 99 | if md.Limit != 0 { 100 | t.Errorf("Limit for new channel not zero.") 101 | } 102 | 103 | // enable limit correctly 104 | ch.parseModes("+l", "256") 105 | compareChannel(t, ch) 106 | if md.Limit != 256 { 107 | t.Errorf("Limit for channel not set correctly") 108 | } 109 | 110 | // enable limit incorrectly 111 | ch.parseModes("+l") 112 | compareChannel(t, ch) 113 | if md.Limit != 256 { 114 | t.Errorf("Bad limit value caused limit to be unset.") 115 | } 116 | 117 | // disable limit correctly 118 | ch.parseModes("-l") 119 | compareChannel(t, ch) 120 | if md.Limit != 0 { 121 | t.Errorf("Limit for channel not unset correctly") 122 | } 123 | 124 | // Test string parsing (currently only channel key) 125 | if md.Key != "" { 126 | t.Errorf("Key set for new channel.") 127 | } 128 | 129 | // enable key correctly 130 | ch.parseModes("+k", "foobar") 131 | compareChannel(t, ch) 132 | if md.Key != "foobar" { 133 | t.Errorf("Key for channel not set correctly") 134 | } 135 | 136 | // enable key incorrectly 137 | ch.parseModes("+k") 138 | compareChannel(t, ch) 139 | if md.Key != "foobar" { 140 | t.Errorf("Bad key value caused key to be unset.") 141 | } 142 | 143 | // disable key correctly 144 | ch.parseModes("-k") 145 | compareChannel(t, ch) 146 | if md.Key != "" { 147 | t.Errorf("Key for channel not unset correctly") 148 | } 149 | 150 | // Test chan privs parsing. 151 | cp.Op = true 152 | cp.HalfOp = true 153 | ch.parseModes("+aq-o", "test1", "test1", "test1") 154 | 155 | compareChannel(t, ch) 156 | if !cp.Owner || !cp.Admin || cp.Op || !cp.HalfOp || cp.Voice { 157 | t.Errorf("Channel privileges not flipped correctly by ParseModes.") 158 | } 159 | 160 | // Test a random mix of modes, just to be sure 161 | md.Limit = 256 162 | ch.parseModes("+zpt-qsl+kv-h", "test1", "foobar", "test1") 163 | 164 | compareChannel(t, ch) 165 | if !md.Private || md.Secret || !md.ProtectedTopic || !md.NoExternalMsg || 166 | !md.Moderated || md.InviteOnly || md.OperOnly || !md.SSLOnly { 167 | t.Errorf("Modes not flipped correctly by ParseModes (2).") 168 | } 169 | if md.Limit != 0 || md.Key != "foobar" { 170 | t.Errorf("Key and limit not changed correctly by ParseModes (2).") 171 | } 172 | if cp.Owner || !cp.Admin || cp.Op || !cp.HalfOp || !cp.Voice { 173 | // NOTE: HalfOp not actually unset above thanks to deliberate error. 174 | t.Errorf("Channel privileges not flipped correctly by ParseModes (2).") 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /state/mock_tracker.go: -------------------------------------------------------------------------------- 1 | // Automatically generated by MockGen. DO NOT EDIT! 2 | // Source: tracker.go 3 | 4 | package state 5 | 6 | import ( 7 | gomock "github.com/golang/mock/gomock" 8 | ) 9 | 10 | // Mock of Tracker interface 11 | type MockTracker struct { 12 | ctrl *gomock.Controller 13 | recorder *_MockTrackerRecorder 14 | } 15 | 16 | // Recorder for MockTracker (not exported) 17 | type _MockTrackerRecorder struct { 18 | mock *MockTracker 19 | } 20 | 21 | func NewMockTracker(ctrl *gomock.Controller) *MockTracker { 22 | mock := &MockTracker{ctrl: ctrl} 23 | mock.recorder = &_MockTrackerRecorder{mock} 24 | return mock 25 | } 26 | 27 | func (_m *MockTracker) EXPECT() *_MockTrackerRecorder { 28 | return _m.recorder 29 | } 30 | 31 | func (_m *MockTracker) NewNick(nick string) *Nick { 32 | ret := _m.ctrl.Call(_m, "NewNick", nick) 33 | ret0, _ := ret[0].(*Nick) 34 | return ret0 35 | } 36 | 37 | func (_mr *_MockTrackerRecorder) NewNick(arg0 interface{}) *gomock.Call { 38 | return _mr.mock.ctrl.RecordCall(_mr.mock, "NewNick", arg0) 39 | } 40 | 41 | func (_m *MockTracker) GetNick(nick string) *Nick { 42 | ret := _m.ctrl.Call(_m, "GetNick", nick) 43 | ret0, _ := ret[0].(*Nick) 44 | return ret0 45 | } 46 | 47 | func (_mr *_MockTrackerRecorder) GetNick(arg0 interface{}) *gomock.Call { 48 | return _mr.mock.ctrl.RecordCall(_mr.mock, "GetNick", arg0) 49 | } 50 | 51 | func (_m *MockTracker) ReNick(old string, neu string) *Nick { 52 | ret := _m.ctrl.Call(_m, "ReNick", old, neu) 53 | ret0, _ := ret[0].(*Nick) 54 | return ret0 55 | } 56 | 57 | func (_mr *_MockTrackerRecorder) ReNick(arg0, arg1 interface{}) *gomock.Call { 58 | return _mr.mock.ctrl.RecordCall(_mr.mock, "ReNick", arg0, arg1) 59 | } 60 | 61 | func (_m *MockTracker) DelNick(nick string) *Nick { 62 | ret := _m.ctrl.Call(_m, "DelNick", nick) 63 | ret0, _ := ret[0].(*Nick) 64 | return ret0 65 | } 66 | 67 | func (_mr *_MockTrackerRecorder) DelNick(arg0 interface{}) *gomock.Call { 68 | return _mr.mock.ctrl.RecordCall(_mr.mock, "DelNick", arg0) 69 | } 70 | 71 | func (_m *MockTracker) NickInfo(nick string, ident string, host string, name string) *Nick { 72 | ret := _m.ctrl.Call(_m, "NickInfo", nick, ident, host, name) 73 | ret0, _ := ret[0].(*Nick) 74 | return ret0 75 | } 76 | 77 | func (_mr *_MockTrackerRecorder) NickInfo(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { 78 | return _mr.mock.ctrl.RecordCall(_mr.mock, "NickInfo", arg0, arg1, arg2, arg3) 79 | } 80 | 81 | func (_m *MockTracker) NickModes(nick string, modestr string) *Nick { 82 | ret := _m.ctrl.Call(_m, "NickModes", nick, modestr) 83 | ret0, _ := ret[0].(*Nick) 84 | return ret0 85 | } 86 | 87 | func (_mr *_MockTrackerRecorder) NickModes(arg0, arg1 interface{}) *gomock.Call { 88 | return _mr.mock.ctrl.RecordCall(_mr.mock, "NickModes", arg0, arg1) 89 | } 90 | 91 | func (_m *MockTracker) NewChannel(channel string) *Channel { 92 | ret := _m.ctrl.Call(_m, "NewChannel", channel) 93 | ret0, _ := ret[0].(*Channel) 94 | return ret0 95 | } 96 | 97 | func (_mr *_MockTrackerRecorder) NewChannel(arg0 interface{}) *gomock.Call { 98 | return _mr.mock.ctrl.RecordCall(_mr.mock, "NewChannel", arg0) 99 | } 100 | 101 | func (_m *MockTracker) GetChannel(channel string) *Channel { 102 | ret := _m.ctrl.Call(_m, "GetChannel", channel) 103 | ret0, _ := ret[0].(*Channel) 104 | return ret0 105 | } 106 | 107 | func (_mr *_MockTrackerRecorder) GetChannel(arg0 interface{}) *gomock.Call { 108 | return _mr.mock.ctrl.RecordCall(_mr.mock, "GetChannel", arg0) 109 | } 110 | 111 | func (_m *MockTracker) DelChannel(channel string) *Channel { 112 | ret := _m.ctrl.Call(_m, "DelChannel", channel) 113 | ret0, _ := ret[0].(*Channel) 114 | return ret0 115 | } 116 | 117 | func (_mr *_MockTrackerRecorder) DelChannel(arg0 interface{}) *gomock.Call { 118 | return _mr.mock.ctrl.RecordCall(_mr.mock, "DelChannel", arg0) 119 | } 120 | 121 | func (_m *MockTracker) Topic(channel string, topic string) *Channel { 122 | ret := _m.ctrl.Call(_m, "Topic", channel, topic) 123 | ret0, _ := ret[0].(*Channel) 124 | return ret0 125 | } 126 | 127 | func (_mr *_MockTrackerRecorder) Topic(arg0, arg1 interface{}) *gomock.Call { 128 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Topic", arg0, arg1) 129 | } 130 | 131 | func (_m *MockTracker) ChannelModes(channel string, modestr string, modeargs ...string) *Channel { 132 | _s := []interface{}{channel, modestr} 133 | for _, _x := range modeargs { 134 | _s = append(_s, _x) 135 | } 136 | ret := _m.ctrl.Call(_m, "ChannelModes", _s...) 137 | ret0, _ := ret[0].(*Channel) 138 | return ret0 139 | } 140 | 141 | func (_mr *_MockTrackerRecorder) ChannelModes(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { 142 | _s := append([]interface{}{arg0, arg1}, arg2...) 143 | return _mr.mock.ctrl.RecordCall(_mr.mock, "ChannelModes", _s...) 144 | } 145 | 146 | func (_m *MockTracker) Me() *Nick { 147 | ret := _m.ctrl.Call(_m, "Me") 148 | ret0, _ := ret[0].(*Nick) 149 | return ret0 150 | } 151 | 152 | func (_mr *_MockTrackerRecorder) Me() *gomock.Call { 153 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Me") 154 | } 155 | 156 | func (_m *MockTracker) IsOn(channel string, nick string) (*ChanPrivs, bool) { 157 | ret := _m.ctrl.Call(_m, "IsOn", channel, nick) 158 | ret0, _ := ret[0].(*ChanPrivs) 159 | ret1, _ := ret[1].(bool) 160 | return ret0, ret1 161 | } 162 | 163 | func (_mr *_MockTrackerRecorder) IsOn(arg0, arg1 interface{}) *gomock.Call { 164 | return _mr.mock.ctrl.RecordCall(_mr.mock, "IsOn", arg0, arg1) 165 | } 166 | 167 | func (_m *MockTracker) Associate(channel string, nick string) *ChanPrivs { 168 | ret := _m.ctrl.Call(_m, "Associate", channel, nick) 169 | ret0, _ := ret[0].(*ChanPrivs) 170 | return ret0 171 | } 172 | 173 | func (_mr *_MockTrackerRecorder) Associate(arg0, arg1 interface{}) *gomock.Call { 174 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Associate", arg0, arg1) 175 | } 176 | 177 | func (_m *MockTracker) Dissociate(channel string, nick string) { 178 | _m.ctrl.Call(_m, "Dissociate", channel, nick) 179 | } 180 | 181 | func (_mr *_MockTrackerRecorder) Dissociate(arg0, arg1 interface{}) *gomock.Call { 182 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Dissociate", arg0, arg1) 183 | } 184 | 185 | func (_m *MockTracker) Wipe() { 186 | _m.ctrl.Call(_m, "Wipe") 187 | } 188 | 189 | func (_mr *_MockTrackerRecorder) Wipe() *gomock.Call { 190 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Wipe") 191 | } 192 | 193 | func (_m *MockTracker) String() string { 194 | ret := _m.ctrl.Call(_m, "String") 195 | ret0, _ := ret[0].(string) 196 | return ret0 197 | } 198 | 199 | func (_mr *_MockTrackerRecorder) String() *gomock.Call { 200 | return _mr.mock.ctrl.RecordCall(_mr.mock, "String") 201 | } 202 | -------------------------------------------------------------------------------- /state/nick.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "github.com/fluffle/goirc/logging" 5 | 6 | "reflect" 7 | ) 8 | 9 | // A Nick is returned from the state tracker and contains 10 | // a copy of the nick state at a particular time. 11 | type Nick struct { 12 | Nick, Ident, Host, Name string 13 | Modes *NickMode 14 | Channels map[string]*ChanPrivs 15 | } 16 | 17 | // Internal bookkeeping struct for nicks. 18 | type nick struct { 19 | nick, ident, host, name string 20 | modes *NickMode 21 | lookup map[string]*channel 22 | chans map[*channel]*ChanPrivs 23 | } 24 | 25 | // A struct representing the modes of an IRC Nick (User Modes) 26 | // (again, only the ones we care about) 27 | // 28 | // This is only really useful for me, as we can't see other people's modes 29 | // without IRC operator privileges (and even then only on some IRCd's). 30 | type NickMode struct { 31 | // MODE +B, +i, +o, +w, +x, +z 32 | Bot, Invisible, Oper, WallOps, HiddenHost, SSL bool 33 | } 34 | 35 | // Map *irc.NickMode fields to IRC mode characters and vice versa 36 | var StringToNickMode = map[string]string{} 37 | var NickModeToString = map[string]string{ 38 | "Bot": "B", 39 | "Invisible": "i", 40 | "Oper": "o", 41 | "WallOps": "w", 42 | "HiddenHost": "x", 43 | "SSL": "z", 44 | } 45 | 46 | func init() { 47 | for k, v := range NickModeToString { 48 | StringToNickMode[v] = k 49 | } 50 | } 51 | 52 | /******************************************************************************\ 53 | * nick methods for state management 54 | \******************************************************************************/ 55 | 56 | func newNick(n string) *nick { 57 | return &nick{ 58 | nick: n, 59 | modes: new(NickMode), 60 | chans: make(map[*channel]*ChanPrivs), 61 | lookup: make(map[string]*channel), 62 | } 63 | } 64 | 65 | // Returns a copy of the internal tracker nick state at this time. 66 | // Relies on tracker-level locking for concurrent access. 67 | func (nk *nick) Nick() *Nick { 68 | n := &Nick{ 69 | Nick: nk.nick, 70 | Ident: nk.ident, 71 | Host: nk.host, 72 | Name: nk.name, 73 | Modes: nk.modes.Copy(), 74 | Channels: make(map[string]*ChanPrivs, len(nk.chans)), 75 | } 76 | for c, cp := range nk.chans { 77 | n.Channels[c.name] = cp.Copy() 78 | } 79 | return n 80 | } 81 | 82 | func (nk *nick) isOn(ch *channel) (*ChanPrivs, bool) { 83 | cp, ok := nk.chans[ch] 84 | return cp.Copy(), ok 85 | } 86 | 87 | // Associates a Channel with a Nick. 88 | func (nk *nick) addChannel(ch *channel, cp *ChanPrivs) { 89 | if _, ok := nk.chans[ch]; !ok { 90 | nk.chans[ch] = cp 91 | nk.lookup[ch.name] = ch 92 | } else { 93 | logging.Warn("Nick.addChannel(): %s already on %s.", nk.nick, ch.name) 94 | } 95 | } 96 | 97 | // Disassociates a Channel from a Nick. 98 | func (nk *nick) delChannel(ch *channel) { 99 | if _, ok := nk.chans[ch]; ok { 100 | delete(nk.chans, ch) 101 | delete(nk.lookup, ch.name) 102 | } else { 103 | logging.Warn("Nick.delChannel(): %s not on %s.", nk.nick, ch.name) 104 | } 105 | } 106 | 107 | // Parse mode strings for a Nick. 108 | func (nk *nick) parseModes(modes string) { 109 | var modeop bool // true => add mode, false => remove mode 110 | for i := 0; i < len(modes); i++ { 111 | switch m := modes[i]; m { 112 | case '+': 113 | modeop = true 114 | case '-': 115 | modeop = false 116 | case 'B': 117 | nk.modes.Bot = modeop 118 | case 'i': 119 | nk.modes.Invisible = modeop 120 | case 'o': 121 | nk.modes.Oper = modeop 122 | case 'w': 123 | nk.modes.WallOps = modeop 124 | case 'x': 125 | nk.modes.HiddenHost = modeop 126 | case 'z': 127 | nk.modes.SSL = modeop 128 | default: 129 | logging.Info("Nick.ParseModes(): unknown mode char %c", m) 130 | } 131 | } 132 | } 133 | 134 | // Returns true if the Nick is associated with the Channel. 135 | func (nk *Nick) IsOn(ch string) (*ChanPrivs, bool) { 136 | cp, ok := nk.Channels[ch] 137 | return cp, ok 138 | } 139 | 140 | // Tests Nick equality. 141 | func (nk *Nick) Equals(other *Nick) bool { 142 | return reflect.DeepEqual(nk, other) 143 | } 144 | 145 | // Duplicates a NickMode struct. 146 | func (nm *NickMode) Copy() *NickMode { 147 | if nm == nil { 148 | return nil 149 | } 150 | n := *nm 151 | return &n 152 | } 153 | 154 | // Tests NickMode equality. 155 | func (nm *NickMode) Equals(other *NickMode) bool { 156 | return reflect.DeepEqual(nm, other) 157 | } 158 | 159 | // Returns a string representing the nick. Looks like: 160 | // 161 | // Nick: e.g. CowMaster 162 | // Hostmask: e.g. moo@cows.org 163 | // Real Name: e.g. Steve "CowMaster" Bush 164 | // Modes: e.g. +z 165 | // Channels: 166 | // : e.g. #moo: +o 167 | // ... 168 | func (nk *Nick) String() string { 169 | str := "Nick: " + nk.Nick + "\n\t" 170 | str += "Hostmask: " + nk.Ident + "@" + nk.Host + "\n\t" 171 | str += "Real Name: " + nk.Name + "\n\t" 172 | str += "Modes: " + nk.Modes.String() + "\n\t" 173 | str += "Channels: \n" 174 | for ch, cp := range nk.Channels { 175 | str += "\t\t" + ch + ": " + cp.String() + "\n" 176 | } 177 | return str 178 | } 179 | 180 | func (nk *nick) String() string { 181 | return nk.Nick().String() 182 | } 183 | 184 | // Returns a string representing the nick modes. Looks like: 185 | // 186 | // +iwx 187 | func (nm *NickMode) String() string { 188 | if nm == nil { 189 | return "No modes set" 190 | } 191 | str := "+" 192 | v := reflect.Indirect(reflect.ValueOf(nm)) 193 | t := v.Type() 194 | for i := 0; i < v.NumField(); i++ { 195 | switch f := v.Field(i); f.Kind() { 196 | // only bools here at the mo! 197 | case reflect.Bool: 198 | if f.Bool() { 199 | str += NickModeToString[t.Field(i).Name] 200 | } 201 | } 202 | } 203 | if str == "+" { 204 | str = "No modes set" 205 | } 206 | return str 207 | } 208 | -------------------------------------------------------------------------------- /state/nick_test.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func compareNick(t *testing.T, nk *nick) { 9 | n := nk.Nick() 10 | if n.Nick != nk.nick || n.Ident != nk.ident || n.Host != nk.host || n.Name != nk.name || 11 | !n.Modes.Equals(nk.modes) || len(n.Channels) != len(nk.chans) { 12 | t.Errorf("Nick not duped correctly from internal state.") 13 | } 14 | for ch, cp := range nk.chans { 15 | if other, ok := n.Channels[ch.name]; !ok || !cp.Equals(other) { 16 | t.Errorf("Channel not duped correctly from internal state.") 17 | } 18 | } 19 | } 20 | 21 | func TestNewNick(t *testing.T) { 22 | nk := newNick("test1") 23 | 24 | if nk.nick != "test1" { 25 | t.Errorf("Nick not created correctly by NewNick()") 26 | } 27 | if len(nk.chans) != 0 || len(nk.lookup) != 0 { 28 | t.Errorf("Nick maps contain data after NewNick()") 29 | } 30 | compareNick(t, nk) 31 | } 32 | 33 | func TestAddChannel(t *testing.T) { 34 | nk := newNick("test1") 35 | ch := newChannel("#test1") 36 | cp := new(ChanPrivs) 37 | 38 | nk.addChannel(ch, cp) 39 | 40 | if len(nk.chans) != 1 || len(nk.lookup) != 1 { 41 | t.Errorf("Channel lists not updated correctly for add.") 42 | } 43 | if c, ok := nk.chans[ch]; !ok || c != cp { 44 | t.Errorf("Channel #test1 not properly stored in chans map.") 45 | } 46 | if c, ok := nk.lookup["#test1"]; !ok || c != ch { 47 | t.Errorf("Channel #test1 not properly stored in lookup map.") 48 | } 49 | compareNick(t, nk) 50 | } 51 | 52 | func TestDelChannel(t *testing.T) { 53 | nk := newNick("test1") 54 | ch := newChannel("#test1") 55 | cp := new(ChanPrivs) 56 | 57 | nk.addChannel(ch, cp) 58 | nk.delChannel(ch) 59 | if len(nk.chans) != 0 || len(nk.lookup) != 0 { 60 | t.Errorf("Channel lists not updated correctly for del.") 61 | } 62 | if c, ok := nk.chans[ch]; ok || c != nil { 63 | t.Errorf("Channel #test1 not properly removed from chans map.") 64 | } 65 | if c, ok := nk.lookup["#test1"]; ok || c != nil { 66 | t.Errorf("Channel #test1 not properly removed from lookup map.") 67 | } 68 | compareNick(t, nk) 69 | } 70 | 71 | func TestNickParseModes(t *testing.T) { 72 | nk := newNick("test1") 73 | md := nk.modes 74 | 75 | // Modes should all be false for a new nick 76 | if md.Invisible || md.Oper || md.WallOps || md.HiddenHost || md.SSL { 77 | t.Errorf("Modes for new nick set to true.") 78 | } 79 | 80 | // Set a couple of modes, for testing. 81 | md.Invisible = true 82 | md.HiddenHost = true 83 | 84 | // Parse a mode line that flips one true to false and two false to true 85 | nk.parseModes("+z-x+w") 86 | 87 | compareNick(t, nk) 88 | if !md.Invisible || md.Oper || !md.WallOps || md.HiddenHost || !md.SSL { 89 | t.Errorf("Modes not flipped correctly by ParseModes.") 90 | } 91 | } 92 | 93 | func BenchmarkNickSingleChan(b *testing.B) { 94 | nk := newNick("test1") 95 | ch := newChannel("#test1") 96 | cp := new(ChanPrivs) 97 | nk.addChannel(ch, cp) 98 | 99 | b.ResetTimer() 100 | for i := 0; i < b.N; i++ { 101 | nk.Nick() 102 | } 103 | } 104 | 105 | func BenchmarkNickManyChan(b *testing.B) { 106 | const numChans = 1000 107 | nk := newNick("test1") 108 | for i := 0; i < numChans; i++ { 109 | ch := newChannel(fmt.Sprintf("#test%d", i)) 110 | cp := new(ChanPrivs) 111 | nk.addChannel(ch, cp) 112 | } 113 | 114 | b.ResetTimer() 115 | for i := 0; i < b.N; i++ { 116 | nk.Nick() 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /state/tracker.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "github.com/fluffle/goirc/logging" 5 | 6 | "sync" 7 | ) 8 | 9 | // The state manager interface 10 | type Tracker interface { 11 | // Nick methods 12 | NewNick(nick string) *Nick 13 | GetNick(nick string) *Nick 14 | ReNick(old, neu string) *Nick 15 | DelNick(nick string) *Nick 16 | NickInfo(nick, ident, host, name string) *Nick 17 | NickModes(nick, modestr string) *Nick 18 | // Channel methods 19 | NewChannel(channel string) *Channel 20 | GetChannel(channel string) *Channel 21 | DelChannel(channel string) *Channel 22 | Topic(channel, topic string) *Channel 23 | ChannelModes(channel, modestr string, modeargs ...string) *Channel 24 | // Information about ME! 25 | Me() *Nick 26 | // And the tracking operations 27 | IsOn(channel, nick string) (*ChanPrivs, bool) 28 | Associate(channel, nick string) *ChanPrivs 29 | Dissociate(channel, nick string) 30 | Wipe() 31 | // The state tracker can output a debugging string 32 | String() string 33 | } 34 | 35 | // ... and a struct to implement it ... 36 | type stateTracker struct { 37 | // Map of channels we're on 38 | chans map[string]*channel 39 | // Map of nicks we know about 40 | nicks map[string]*nick 41 | 42 | // We need to keep state on who we are :-) 43 | me *nick 44 | 45 | // And we need to protect against data races *cough*. 46 | mu sync.Mutex 47 | } 48 | 49 | var _ Tracker = (*stateTracker)(nil) 50 | 51 | // ... and a constructor to make it ... 52 | func NewTracker(mynick string) *stateTracker { 53 | st := &stateTracker{ 54 | chans: make(map[string]*channel), 55 | nicks: make(map[string]*nick), 56 | } 57 | st.me = newNick(mynick) 58 | st.nicks[mynick] = st.me 59 | return st 60 | } 61 | 62 | // ... and a method to wipe the state clean. 63 | func (st *stateTracker) Wipe() { 64 | st.mu.Lock() 65 | defer st.mu.Unlock() 66 | // Deleting all the channels implicitly deletes every nick but me. 67 | for _, ch := range st.chans { 68 | st.delChannel(ch) 69 | } 70 | } 71 | 72 | /******************************************************************************\ 73 | * tracker methods to create/look up nicks/channels 74 | \******************************************************************************/ 75 | 76 | // Creates a new nick, initialises it, and stores it so it 77 | // can be properly tracked for state management purposes. 78 | func (st *stateTracker) NewNick(n string) *Nick { 79 | if n == "" { 80 | logging.Warn("Tracker.NewNick(): Not tracking empty nick.") 81 | return nil 82 | } 83 | st.mu.Lock() 84 | defer st.mu.Unlock() 85 | if _, ok := st.nicks[n]; ok { 86 | logging.Warn("Tracker.NewNick(): %s already tracked.", n) 87 | return nil 88 | } 89 | st.nicks[n] = newNick(n) 90 | return st.nicks[n].Nick() 91 | } 92 | 93 | // Returns a nick for the nick n, if we're tracking it. 94 | func (st *stateTracker) GetNick(n string) *Nick { 95 | st.mu.Lock() 96 | defer st.mu.Unlock() 97 | if nk, ok := st.nicks[n]; ok { 98 | return nk.Nick() 99 | } 100 | return nil 101 | } 102 | 103 | // Signals to the tracker that a nick should be tracked 104 | // under a "neu" nick rather than the old one. 105 | func (st *stateTracker) ReNick(old, neu string) *Nick { 106 | st.mu.Lock() 107 | defer st.mu.Unlock() 108 | nk, ok := st.nicks[old] 109 | if !ok { 110 | logging.Warn("Tracker.ReNick(): %s not tracked.", old) 111 | return nil 112 | } 113 | if _, ok := st.nicks[neu]; ok { 114 | logging.Warn("Tracker.ReNick(): %s already exists.", neu) 115 | return nil 116 | } 117 | 118 | nk.nick = neu 119 | delete(st.nicks, old) 120 | st.nicks[neu] = nk 121 | for ch, _ := range nk.chans { 122 | // We also need to update the lookup maps of all the channels 123 | // the nick is on, to keep things in sync. 124 | delete(ch.lookup, old) 125 | ch.lookup[neu] = nk 126 | } 127 | return nk.Nick() 128 | } 129 | 130 | // Removes a nick from being tracked. 131 | func (st *stateTracker) DelNick(n string) *Nick { 132 | st.mu.Lock() 133 | defer st.mu.Unlock() 134 | if nk, ok := st.nicks[n]; ok { 135 | if nk == st.me { 136 | logging.Warn("Tracker.DelNick(): won't delete myself.") 137 | return nil 138 | } 139 | st.delNick(nk) 140 | return nk.Nick() 141 | } 142 | logging.Warn("Tracker.DelNick(): %s not tracked.", n) 143 | return nil 144 | } 145 | 146 | func (st *stateTracker) delNick(nk *nick) { 147 | // st.mu lock held by DelNick, DelChannel or Wipe 148 | if nk == st.me { 149 | // Shouldn't get here => internal state tracking code is fubar. 150 | logging.Error("Tracker.DelNick(): TRYING TO DELETE ME :-(") 151 | return 152 | } 153 | delete(st.nicks, nk.nick) 154 | for ch, _ := range nk.chans { 155 | nk.delChannel(ch) 156 | ch.delNick(nk) 157 | if len(ch.nicks) == 0 { 158 | // Deleting a nick from tracking shouldn't empty any channels as 159 | // *we* should be on the channel with them to be tracking them. 160 | logging.Error("Tracker.delNick(): deleting nick %s emptied "+ 161 | "channel %s, this shouldn't happen!", nk.nick, ch.name) 162 | } 163 | } 164 | } 165 | 166 | // Sets ident, host and "real" name for the nick. 167 | func (st *stateTracker) NickInfo(n, ident, host, name string) *Nick { 168 | st.mu.Lock() 169 | defer st.mu.Unlock() 170 | nk, ok := st.nicks[n] 171 | if !ok { 172 | return nil 173 | } 174 | nk.ident = ident 175 | nk.host = host 176 | nk.name = name 177 | return nk.Nick() 178 | } 179 | 180 | // Sets user modes for the nick. 181 | func (st *stateTracker) NickModes(n, modes string) *Nick { 182 | st.mu.Lock() 183 | defer st.mu.Unlock() 184 | nk, ok := st.nicks[n] 185 | if !ok { 186 | return nil 187 | } 188 | nk.parseModes(modes) 189 | return nk.Nick() 190 | } 191 | 192 | // Creates a new Channel, initialises it, and stores it so it 193 | // can be properly tracked for state management purposes. 194 | func (st *stateTracker) NewChannel(c string) *Channel { 195 | if c == "" { 196 | logging.Warn("Tracker.NewChannel(): Not tracking empty channel.") 197 | return nil 198 | } 199 | st.mu.Lock() 200 | defer st.mu.Unlock() 201 | if _, ok := st.chans[c]; ok { 202 | logging.Warn("Tracker.NewChannel(): %s already tracked.", c) 203 | return nil 204 | } 205 | st.chans[c] = newChannel(c) 206 | return st.chans[c].Channel() 207 | } 208 | 209 | // Returns a Channel for the channel c, if we're tracking it. 210 | func (st *stateTracker) GetChannel(c string) *Channel { 211 | st.mu.Lock() 212 | defer st.mu.Unlock() 213 | if ch, ok := st.chans[c]; ok { 214 | return ch.Channel() 215 | } 216 | return nil 217 | } 218 | 219 | // Removes a Channel from being tracked. 220 | func (st *stateTracker) DelChannel(c string) *Channel { 221 | st.mu.Lock() 222 | defer st.mu.Unlock() 223 | if ch, ok := st.chans[c]; ok { 224 | st.delChannel(ch) 225 | return ch.Channel() 226 | } 227 | logging.Warn("Tracker.DelChannel(): %s not tracked.", c) 228 | return nil 229 | } 230 | 231 | func (st *stateTracker) delChannel(ch *channel) { 232 | // st.mu lock held by DelChannel or Wipe 233 | delete(st.chans, ch.name) 234 | for nk, _ := range ch.nicks { 235 | ch.delNick(nk) 236 | nk.delChannel(ch) 237 | if len(nk.chans) == 0 && nk != st.me { 238 | // We're no longer in any channels with this nick. 239 | st.delNick(nk) 240 | } 241 | } 242 | } 243 | 244 | // Sets the topic of a channel. 245 | func (st *stateTracker) Topic(c, topic string) *Channel { 246 | st.mu.Lock() 247 | defer st.mu.Unlock() 248 | ch, ok := st.chans[c] 249 | if !ok { 250 | return nil 251 | } 252 | ch.topic = topic 253 | return ch.Channel() 254 | } 255 | 256 | // Sets modes for a channel, including privileges like +o. 257 | func (st *stateTracker) ChannelModes(c, modes string, args ...string) *Channel { 258 | st.mu.Lock() 259 | defer st.mu.Unlock() 260 | ch, ok := st.chans[c] 261 | if !ok { 262 | return nil 263 | } 264 | ch.parseModes(modes, args...) 265 | return ch.Channel() 266 | } 267 | 268 | // Returns the Nick the state tracker thinks is Me. 269 | // NOTE: Nick() requires the mutex to be held. 270 | func (st *stateTracker) Me() *Nick { 271 | st.mu.Lock() 272 | defer st.mu.Unlock() 273 | return st.me.Nick() 274 | } 275 | 276 | // Returns true if both the channel c and the nick n are tracked 277 | // and the nick is associated with the channel. 278 | func (st *stateTracker) IsOn(c, n string) (*ChanPrivs, bool) { 279 | st.mu.Lock() 280 | defer st.mu.Unlock() 281 | nk, nok := st.nicks[n] 282 | ch, cok := st.chans[c] 283 | if nok && cok { 284 | return nk.isOn(ch) 285 | } 286 | return nil, false 287 | } 288 | 289 | // Associates an already known nick with an already known channel. 290 | func (st *stateTracker) Associate(c, n string) *ChanPrivs { 291 | st.mu.Lock() 292 | defer st.mu.Unlock() 293 | nk, nok := st.nicks[n] 294 | ch, cok := st.chans[c] 295 | 296 | if !cok { 297 | // As we can implicitly delete both nicks and channels from being 298 | // tracked by dissociating one from the other, we should verify that 299 | // we're not being passed an old Nick or Channel. 300 | logging.Error("Tracker.Associate(): channel %s not found in "+ 301 | "internal state.", c) 302 | return nil 303 | } else if !nok { 304 | logging.Error("Tracker.Associate(): nick %s not found in "+ 305 | "internal state.", n) 306 | return nil 307 | } else if _, ok := nk.isOn(ch); ok { 308 | logging.Warn("Tracker.Associate(): %s already on %s.", 309 | nk, ch) 310 | return nil 311 | } 312 | cp := new(ChanPrivs) 313 | ch.addNick(nk, cp) 314 | nk.addChannel(ch, cp) 315 | return cp.Copy() 316 | } 317 | 318 | // Dissociates an already known nick from an already known channel. 319 | // Does some tidying up to stop tracking nicks we're no longer on 320 | // any common channels with, and channels we're no longer on. 321 | func (st *stateTracker) Dissociate(c, n string) { 322 | st.mu.Lock() 323 | defer st.mu.Unlock() 324 | nk, nok := st.nicks[n] 325 | ch, cok := st.chans[c] 326 | 327 | if !cok { 328 | // As we can implicitly delete both nicks and channels from being 329 | // tracked by dissociating one from the other, we should verify that 330 | // we're not being passed an old Nick or Channel. 331 | logging.Error("Tracker.Dissociate(): channel %s not found in "+ 332 | "internal state.", c) 333 | } else if !nok { 334 | logging.Error("Tracker.Dissociate(): nick %s not found in "+ 335 | "internal state.", n) 336 | } else if _, ok := nk.isOn(ch); !ok { 337 | logging.Warn("Tracker.Dissociate(): %s not on %s.", 338 | nk.nick, ch.name) 339 | } else if nk == st.me { 340 | // I'm leaving the channel for some reason, so it won't be tracked. 341 | st.delChannel(ch) 342 | } else { 343 | // Remove the nick from the channel and the channel from the nick. 344 | ch.delNick(nk) 345 | nk.delChannel(ch) 346 | if len(nk.chans) == 0 { 347 | // We're no longer in any channels with this nick. 348 | st.delNick(nk) 349 | } 350 | } 351 | } 352 | 353 | func (st *stateTracker) String() string { 354 | st.mu.Lock() 355 | defer st.mu.Unlock() 356 | str := "GoIRC Channels\n" 357 | str += "--------------\n\n" 358 | for _, ch := range st.chans { 359 | str += ch.String() + "\n" 360 | } 361 | str += "GoIRC NickNames\n" 362 | str += "---------------\n\n" 363 | for _, n := range st.nicks { 364 | if n != st.me { 365 | str += n.String() + "\n" 366 | } 367 | } 368 | return str 369 | } 370 | -------------------------------------------------------------------------------- /state/tracker_test.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // There is some awkwardness in these tests. Items retrieved directly from the 8 | // state trackers internal maps are private and only have private, 9 | // uncaptialised members. Items retrieved from state tracker public interface 10 | // methods are public and only have public, capitalised members. Comparisons of 11 | // the two are done on the basis of nick or channel name. 12 | 13 | func TestSTNewTracker(t *testing.T) { 14 | st := NewTracker("mynick") 15 | 16 | if len(st.nicks) != 1 { 17 | t.Errorf("Nick list of new tracker is not 1 (me!).") 18 | } 19 | if len(st.chans) != 0 { 20 | t.Errorf("Channel list of new tracker is not empty.") 21 | } 22 | if nk, ok := st.nicks["mynick"]; !ok || nk.nick != "mynick" || nk != st.me { 23 | t.Errorf("My nick not stored correctly in tracker.") 24 | } 25 | } 26 | 27 | func TestSTNewNick(t *testing.T) { 28 | st := NewTracker("mynick") 29 | test1 := st.NewNick("test1") 30 | 31 | if test1 == nil || test1.Nick != "test1" { 32 | t.Errorf("Nick object created incorrectly by NewNick.") 33 | } 34 | if n, ok := st.nicks["test1"]; !ok || !test1.Equals(n.Nick()) || len(st.nicks) != 2 { 35 | t.Errorf("Nick object stored incorrectly by NewNick.") 36 | } 37 | 38 | if fail := st.NewNick("test1"); fail != nil { 39 | t.Errorf("Creating duplicate nick did not produce nil return.") 40 | } 41 | if fail := st.NewNick(""); fail != nil { 42 | t.Errorf("Creating empty nick did not produce nil return.") 43 | } 44 | } 45 | 46 | func TestSTGetNick(t *testing.T) { 47 | st := NewTracker("mynick") 48 | test1 := st.NewNick("test1") 49 | 50 | if n := st.GetNick("test1"); !test1.Equals(n) { 51 | t.Errorf("Incorrect nick returned by GetNick.") 52 | } 53 | if n := st.GetNick("test2"); n != nil { 54 | t.Errorf("Nick unexpectedly returned by GetNick.") 55 | } 56 | if len(st.nicks) != 2 { 57 | t.Errorf("Nick list changed size during GetNick.") 58 | } 59 | } 60 | 61 | func TestSTReNick(t *testing.T) { 62 | st := NewTracker("mynick") 63 | test1 := st.NewNick("test1") 64 | 65 | // This channel is here to ensure that its lookup map gets updated 66 | st.NewChannel("#chan1") 67 | st.Associate("#chan1", "test1") 68 | 69 | // We need to check out the manipulation of the internals. 70 | n1 := st.nicks["test1"] 71 | c1 := st.chans["#chan1"] 72 | 73 | test2 := st.ReNick("test1", "test2") 74 | 75 | if _, ok := st.nicks["test1"]; ok { 76 | t.Errorf("Nick test1 still exists after ReNick.") 77 | } 78 | if n, ok := st.nicks["test2"]; !ok || n != n1 { 79 | t.Errorf("Nick test2 doesn't exist after ReNick.") 80 | } 81 | if _, ok := c1.lookup["test1"]; ok { 82 | t.Errorf("Channel #chan1 still knows about test1 after ReNick.") 83 | } 84 | if n, ok := c1.lookup["test2"]; !ok || n != n1 { 85 | t.Errorf("Channel #chan1 doesn't know about test2 after ReNick.") 86 | } 87 | if test1.Nick != "test1" { 88 | t.Errorf("Nick test1 changed unexpectedly.") 89 | } 90 | if !test2.Equals(n1.Nick()) { 91 | t.Errorf("Nick test2 did not change.") 92 | } 93 | if len(st.nicks) != 2 { 94 | t.Errorf("Nick list changed size during ReNick.") 95 | } 96 | if len(c1.lookup) != 1 { 97 | t.Errorf("Channel lookup list changed size during ReNick.") 98 | } 99 | 100 | st.NewNick("test1") 101 | n2 := st.nicks["test1"] 102 | fail := st.ReNick("test1", "test2") 103 | 104 | if n, ok := st.nicks["test2"]; !ok || n != n1 { 105 | t.Errorf("Nick test2 overwritten/deleted by ReNick.") 106 | } 107 | if n, ok := st.nicks["test1"]; !ok || n != n2 { 108 | t.Errorf("Nick test1 overwritten/deleted by ReNick.") 109 | } 110 | if fail != nil { 111 | t.Errorf("ReNick returned Nick on failure.") 112 | } 113 | if len(st.nicks) != 3 { 114 | t.Errorf("Nick list changed size during ReNick.") 115 | } 116 | } 117 | 118 | func TestSTDelNick(t *testing.T) { 119 | st := NewTracker("mynick") 120 | 121 | add := st.NewNick("test1") 122 | del := st.DelNick("test1") 123 | 124 | if _, ok := st.nicks["test1"]; ok { 125 | t.Errorf("Nick test1 still exists after DelNick.") 126 | } 127 | if len(st.nicks) != 1 { 128 | t.Errorf("Nick list still contains nicks after DelNick.") 129 | } 130 | if !add.Equals(del) { 131 | t.Errorf("DelNick returned different nick.") 132 | } 133 | 134 | // Deleting unknown nick shouldn't work, but let's make sure we have a 135 | // known nick first to catch any possible accidental removals. 136 | st.NewNick("test1") 137 | fail := st.DelNick("test2") 138 | if fail != nil || len(st.nicks) != 2 { 139 | t.Errorf("Deleting unknown nick had unexpected side-effects.") 140 | } 141 | 142 | // Deleting my nick shouldn't work 143 | fail = st.DelNick("mynick") 144 | if fail != nil || len(st.nicks) != 2 { 145 | t.Errorf("Deleting myself had unexpected side-effects.") 146 | } 147 | 148 | // Test that deletion correctly dissociates nick from channels. 149 | // NOTE: the two error states in delNick (as opposed to DelNick) 150 | // are not tested for here, as they will only arise from programming 151 | // errors in other methods. 152 | 153 | // Create a new channel for testing purposes. 154 | st.NewChannel("#test1") 155 | 156 | // Associate both "my" nick and test1 with the channel 157 | st.Associate("#test1", "mynick") 158 | st.Associate("#test1", "test1") 159 | 160 | // We need to check out the manipulation of the internals. 161 | n1 := st.nicks["test1"] 162 | c1 := st.chans["#test1"] 163 | 164 | // Test we have the expected starting state (at least vaguely) 165 | if len(c1.nicks) != 2 || len(st.nicks) != 2 || 166 | len(st.me.chans) != 1 || len(n1.chans) != 1 || len(st.chans) != 1 { 167 | t.Errorf("Bad initial state for test DelNick() channel dissociation.") 168 | } 169 | 170 | // Actual deletion tested above... 171 | st.DelNick("test1") 172 | 173 | if len(c1.nicks) != 1 || len(st.nicks) != 1 || 174 | len(st.me.chans) != 1 || len(n1.chans) != 0 || len(st.chans) != 1 { 175 | t.Errorf("Deleting nick didn't dissociate correctly from channels.") 176 | } 177 | 178 | if _, ok := c1.nicks[n1]; ok { 179 | t.Errorf("Nick not removed from channel's nick map.") 180 | } 181 | if _, ok := c1.lookup["test1"]; ok { 182 | t.Errorf("Nick not removed from channel's lookup map.") 183 | } 184 | } 185 | 186 | func TestSTNickInfo(t *testing.T) { 187 | st := NewTracker("mynick") 188 | test1 := st.NewNick("test1") 189 | test2 := st.NickInfo("test1", "foo", "bar", "baz") 190 | test3 := st.GetNick("test1") 191 | 192 | if test1.Equals(test2) { 193 | t.Errorf("NickInfo did not return modified nick.") 194 | } 195 | if !test3.Equals(test2) { 196 | t.Errorf("Getting nick after NickInfo returned different nick.") 197 | } 198 | test1.Ident, test1.Host, test1.Name = "foo", "bar", "baz" 199 | if !test1.Equals(test2) { 200 | t.Errorf("NickInfo did not set nick info correctly.") 201 | } 202 | 203 | if fail := st.NickInfo("test2", "foo", "bar", "baz"); fail != nil { 204 | t.Errorf("NickInfo for nonexistent nick did not return nil.") 205 | } 206 | } 207 | 208 | func TestSTNickModes(t *testing.T) { 209 | st := NewTracker("mynick") 210 | test1 := st.NewNick("test1") 211 | test2 := st.NickModes("test1", "+iB") 212 | test3 := st.GetNick("test1") 213 | 214 | if test1.Equals(test2) { 215 | t.Errorf("NickModes did not return modified nick.") 216 | } 217 | if !test3.Equals(test2) { 218 | t.Errorf("Getting nick after NickModes returned different nick.") 219 | } 220 | test1.Modes.Invisible, test1.Modes.Bot = true, true 221 | if !test1.Equals(test2) { 222 | t.Errorf("NickModes did not set nick modes correctly.") 223 | } 224 | 225 | if fail := st.NickModes("test2", "whatevs"); fail != nil { 226 | t.Errorf("NickModes for nonexistent nick did not return nil.") 227 | } 228 | } 229 | 230 | func TestSTNewChannel(t *testing.T) { 231 | st := NewTracker("mynick") 232 | 233 | if len(st.chans) != 0 { 234 | t.Errorf("Channel list of new tracker is non-zero length.") 235 | } 236 | 237 | test1 := st.NewChannel("#test1") 238 | 239 | if test1 == nil || test1.Name != "#test1" { 240 | t.Errorf("Channel object created incorrectly by NewChannel.") 241 | } 242 | if c, ok := st.chans["#test1"]; !ok || !test1.Equals(c.Channel()) || len(st.chans) != 1 { 243 | t.Errorf("Channel object stored incorrectly by NewChannel.") 244 | } 245 | 246 | if fail := st.NewChannel("#test1"); fail != nil { 247 | t.Errorf("Creating duplicate chan did not produce nil return.") 248 | } 249 | if fail := st.NewChannel(""); fail != nil { 250 | t.Errorf("Creating empty chan did not produce nil return.") 251 | } 252 | } 253 | 254 | func TestSTGetChannel(t *testing.T) { 255 | st := NewTracker("mynick") 256 | 257 | test1 := st.NewChannel("#test1") 258 | 259 | if c := st.GetChannel("#test1"); !test1.Equals(c) { 260 | t.Errorf("Incorrect Channel returned by GetChannel.") 261 | } 262 | if c := st.GetChannel("#test2"); c != nil { 263 | t.Errorf("Channel unexpectedly returned by GetChannel.") 264 | } 265 | if len(st.chans) != 1 { 266 | t.Errorf("Channel list changed size during GetChannel.") 267 | } 268 | } 269 | 270 | func TestSTDelChannel(t *testing.T) { 271 | st := NewTracker("mynick") 272 | 273 | add := st.NewChannel("#test1") 274 | del := st.DelChannel("#test1") 275 | 276 | if _, ok := st.chans["#test1"]; ok { 277 | t.Errorf("Channel test1 still exists after DelChannel.") 278 | } 279 | if len(st.chans) != 0 { 280 | t.Errorf("Channel list still contains chans after DelChannel.") 281 | } 282 | if !add.Equals(del) { 283 | t.Errorf("DelChannel returned different channel.") 284 | } 285 | 286 | // Deleting unknown channel shouldn't work, but let's make sure we have a 287 | // known channel first to catch any possible accidental removals. 288 | st.NewChannel("#test1") 289 | fail := st.DelChannel("#test2") 290 | if fail != nil || len(st.chans) != 1 { 291 | t.Errorf("DelChannel had unexpected side-effects.") 292 | } 293 | 294 | // Test that deletion correctly dissociates channel from tracked nicks. 295 | // In order to test this thoroughly we need two channels (so that delNick() 296 | // is not called internally in delChannel() when len(nick1.chans) == 0. 297 | st.NewChannel("#test2") 298 | st.NewNick("test1") 299 | 300 | // Associate both "my" nick and test1 with the channels 301 | st.Associate("#test1", "mynick") 302 | st.Associate("#test1", "test1") 303 | st.Associate("#test2", "mynick") 304 | st.Associate("#test2", "test1") 305 | 306 | // We need to check out the manipulation of the internals. 307 | n1 := st.nicks["test1"] 308 | c1 := st.chans["#test1"] 309 | c2 := st.chans["#test2"] 310 | 311 | // Test we have the expected starting state (at least vaguely) 312 | if len(c1.nicks) != 2 || len(c2.nicks) != 2 || len(st.nicks) != 2 || 313 | len(st.me.chans) != 2 || len(n1.chans) != 2 || len(st.chans) != 2 { 314 | t.Errorf("Bad initial state for test DelChannel() nick dissociation.") 315 | } 316 | 317 | st.DelChannel("#test1") 318 | 319 | // Test intermediate state. We're still on #test2 with test1, so test1 320 | // shouldn't be deleted from state tracking itself just yet. 321 | if len(c1.nicks) != 0 || len(c2.nicks) != 2 || len(st.nicks) != 2 || 322 | len(st.me.chans) != 1 || len(n1.chans) != 1 || len(st.chans) != 1 { 323 | t.Errorf("Deleting channel didn't dissociate correctly from nicks.") 324 | } 325 | if _, ok := n1.chans[c1]; ok { 326 | t.Errorf("Channel not removed from nick's chans map.") 327 | } 328 | if _, ok := n1.lookup["#test1"]; ok { 329 | t.Errorf("Channel not removed from nick's lookup map.") 330 | } 331 | 332 | st.DelChannel("#test2") 333 | 334 | // Test final state. Deleting #test2 means that we're no longer on any 335 | // common channels with test1, and thus it should be removed from tracking. 336 | if len(c1.nicks) != 0 || len(c2.nicks) != 0 || len(st.nicks) != 1 || 337 | len(st.me.chans) != 0 || len(n1.chans) != 0 || len(st.chans) != 0 { 338 | t.Errorf("Deleting last channel didn't dissociate correctly from nicks.") 339 | } 340 | if _, ok := st.nicks["test1"]; ok { 341 | t.Errorf("Nick not deleted correctly when on no channels.") 342 | } 343 | if _, ok := st.nicks["mynick"]; !ok { 344 | t.Errorf("My nick deleted incorrectly when on no channels.") 345 | } 346 | } 347 | 348 | func TestSTTopic(t *testing.T) { 349 | st := NewTracker("mynick") 350 | test1 := st.NewChannel("#test1") 351 | test2 := st.Topic("#test1", "foo bar") 352 | test3 := st.GetChannel("#test1") 353 | 354 | if test1.Equals(test2) { 355 | t.Errorf("Topic did not return modified channel.") 356 | } 357 | if !test3.Equals(test2) { 358 | t.Errorf("Getting channel after Topic returned different channel.") 359 | } 360 | test1.Topic = "foo bar" 361 | if !test1.Equals(test2) { 362 | t.Errorf("Topic did not set channel topic correctly.") 363 | } 364 | 365 | if fail := st.Topic("#test2", "foo baz"); fail != nil { 366 | t.Errorf("Topic for nonexistent channel did not return nil.") 367 | } 368 | } 369 | 370 | func TestSTChannelModes(t *testing.T) { 371 | st := NewTracker("mynick") 372 | test1 := st.NewChannel("#test1") 373 | test2 := st.ChannelModes("#test1", "+sk", "foo") 374 | test3 := st.GetChannel("#test1") 375 | 376 | if test1.Equals(test2) { 377 | t.Errorf("ChannelModes did not return modified channel.") 378 | } 379 | if !test3.Equals(test2) { 380 | t.Errorf("Getting channel after ChannelModes returned different channel.") 381 | } 382 | test1.Modes.Secret, test1.Modes.Key = true, "foo" 383 | if !test1.Equals(test2) { 384 | t.Errorf("ChannelModes did not set channel modes correctly.") 385 | } 386 | 387 | if fail := st.ChannelModes("test2", "whatevs"); fail != nil { 388 | t.Errorf("ChannelModes for nonexistent channel did not return nil.") 389 | } 390 | } 391 | 392 | func TestSTIsOn(t *testing.T) { 393 | st := NewTracker("mynick") 394 | 395 | st.NewNick("test1") 396 | st.NewChannel("#test1") 397 | 398 | if priv, ok := st.IsOn("#test1", "test1"); ok || priv != nil { 399 | t.Errorf("test1 is not on #test1 (yet)") 400 | } 401 | st.Associate("#test1", "test1") 402 | if priv, ok := st.IsOn("#test1", "test1"); !ok || priv == nil { 403 | t.Errorf("test1 is on #test1 (now)") 404 | } 405 | } 406 | 407 | func TestSTAssociate(t *testing.T) { 408 | st := NewTracker("mynick") 409 | 410 | st.NewNick("test1") 411 | st.NewChannel("#test1") 412 | 413 | // We need to check out the manipulation of the internals. 414 | n1 := st.nicks["test1"] 415 | c1 := st.chans["#test1"] 416 | 417 | st.Associate("#test1", "test1") 418 | npriv, nok := n1.chans[c1] 419 | cpriv, cok := c1.nicks[n1] 420 | if !nok || !cok || npriv != cpriv { 421 | t.Errorf("#test1 was not associated with test1.") 422 | } 423 | 424 | // Test error cases 425 | if st.Associate("", "test1") != nil { 426 | t.Errorf("Associating unknown channel did not return nil.") 427 | } 428 | if st.Associate("#test1", "") != nil { 429 | t.Errorf("Associating unknown nick did not return nil.") 430 | } 431 | if st.Associate("#test1", "test1") != nil { 432 | t.Errorf("Associating already-associated things did not return nil.") 433 | } 434 | } 435 | 436 | func TestSTDissociate(t *testing.T) { 437 | st := NewTracker("mynick") 438 | 439 | st.NewNick("test1") 440 | st.NewChannel("#test1") 441 | st.NewChannel("#test2") 442 | 443 | // Associate both "my" nick and test1 with the channels 444 | st.Associate("#test1", "mynick") 445 | st.Associate("#test1", "test1") 446 | st.Associate("#test2", "mynick") 447 | st.Associate("#test2", "test1") 448 | 449 | // We need to check out the manipulation of the internals. 450 | n1 := st.nicks["test1"] 451 | c1 := st.chans["#test1"] 452 | c2 := st.chans["#test2"] 453 | 454 | // Check the initial state looks mostly like we expect it to. 455 | if len(c1.nicks) != 2 || len(c2.nicks) != 2 || len(st.nicks) != 2 || 456 | len(st.me.chans) != 2 || len(n1.chans) != 2 || len(st.chans) != 2 { 457 | t.Errorf("Initial state for dissociation tests looks odd.") 458 | } 459 | 460 | // First, test the case of me leaving #test2 461 | st.Dissociate("#test2", "mynick") 462 | 463 | // This should have resulted in the complete deletion of the channel. 464 | if len(c1.nicks) != 2 || len(c2.nicks) != 0 || len(st.nicks) != 2 || 465 | len(st.me.chans) != 1 || len(n1.chans) != 1 || len(st.chans) != 1 { 466 | t.Errorf("Dissociating myself from channel didn't delete it correctly.") 467 | } 468 | if st.GetChannel("#test2") != nil { 469 | t.Errorf("Able to get channel after dissociating myself.") 470 | } 471 | 472 | // Reassociating myself and test1 to #test2 shouldn't cause any errors. 473 | st.NewChannel("#test2") 474 | st.Associate("#test2", "mynick") 475 | st.Associate("#test2", "test1") 476 | 477 | // c2 is out of date with the complete deletion of the channel 478 | c2 = st.chans["#test2"] 479 | 480 | // Check state once moar. 481 | if len(c1.nicks) != 2 || len(c2.nicks) != 2 || len(st.nicks) != 2 || 482 | len(st.me.chans) != 2 || len(n1.chans) != 2 || len(st.chans) != 2 { 483 | t.Errorf("Reassociating to channel has produced unexpected state.") 484 | } 485 | 486 | // Now, lets dissociate test1 from #test1 then #test2. 487 | // This first one should only result in a change in associations. 488 | st.Dissociate("#test1", "test1") 489 | 490 | if len(c1.nicks) != 1 || len(c2.nicks) != 2 || len(st.nicks) != 2 || 491 | len(st.me.chans) != 2 || len(n1.chans) != 1 || len(st.chans) != 2 { 492 | t.Errorf("Dissociating a nick from one channel went wrong.") 493 | } 494 | 495 | // This second one should also delete test1 496 | // as it's no longer on any common channels with us 497 | st.Dissociate("#test2", "test1") 498 | 499 | if len(c1.nicks) != 1 || len(c2.nicks) != 1 || len(st.nicks) != 1 || 500 | len(st.me.chans) != 2 || len(n1.chans) != 0 || len(st.chans) != 2 { 501 | t.Errorf("Dissociating a nick from it's last channel went wrong.") 502 | } 503 | if st.GetNick("test1") != nil { 504 | t.Errorf("Able to get nick after dissociating from all channels.") 505 | } 506 | } 507 | 508 | func TestSTWipe(t *testing.T) { 509 | st := NewTracker("mynick") 510 | 511 | st.NewNick("test1") 512 | st.NewNick("test2") 513 | st.NewNick("test3") 514 | st.NewChannel("#test1") 515 | st.NewChannel("#test2") 516 | st.NewChannel("#test3") 517 | 518 | // Some associations 519 | st.Associate("#test1", "mynick") 520 | st.Associate("#test2", "mynick") 521 | st.Associate("#test3", "mynick") 522 | 523 | st.Associate("#test1", "test1") 524 | st.Associate("#test2", "test2") 525 | st.Associate("#test3", "test3") 526 | 527 | st.Associate("#test1", "test2") 528 | st.Associate("#test2", "test3") 529 | 530 | st.Associate("#test1", "test3") 531 | 532 | // We need to check out the manipulation of the internals. 533 | nick1 := st.nicks["test1"] 534 | nick2 := st.nicks["test2"] 535 | nick3 := st.nicks["test3"] 536 | chan1 := st.chans["#test1"] 537 | chan2 := st.chans["#test2"] 538 | chan3 := st.chans["#test3"] 539 | 540 | // Check the state we have at this point is what we would expect. 541 | if len(st.nicks) != 4 || len(st.chans) != 3 || len(st.me.chans) != 3 { 542 | t.Errorf("Tracker nick/channel lists wrong length before wipe.") 543 | } 544 | if len(chan1.nicks) != 4 || len(chan2.nicks) != 3 || len(chan3.nicks) != 2 { 545 | t.Errorf("Channel nick lists wrong length before wipe.") 546 | } 547 | if len(nick1.chans) != 1 || len(nick2.chans) != 2 || len(nick3.chans) != 3 { 548 | t.Errorf("Nick chan lists wrong length before wipe.") 549 | } 550 | 551 | // Nuke *all* the state! 552 | st.Wipe() 553 | 554 | // Check the state we have at this point is what we would expect. 555 | if len(st.nicks) != 1 || len(st.chans) != 0 || len(st.me.chans) != 0 { 556 | t.Errorf("Tracker nick/channel lists wrong length after wipe.") 557 | } 558 | if len(chan1.nicks) != 0 || len(chan2.nicks) != 0 || len(chan3.nicks) != 0 { 559 | t.Errorf("Channel nick lists wrong length after wipe.") 560 | } 561 | if len(nick1.chans) != 0 || len(nick2.chans) != 0 || len(nick3.chans) != 0 { 562 | t.Errorf("Nick chan lists wrong length after wipe.") 563 | } 564 | } 565 | -------------------------------------------------------------------------------- /vims: -------------------------------------------------------------------------------- 1 | find . -name \*.go | xargs gvim -p README.md 2 | --------------------------------------------------------------------------------