From 1503d7b9db46fcfcd032c8d99e4b55cc8ffd45d3 Mon Sep 17 00:00:00 2001 From: Yves Junqueira Date: Sun, 22 Aug 2010 02:23:07 +0800 Subject: [PATCH 01/20] Fixes for recent versions of Go. Simplifying getStringMsg(). --- irc/commands.go | 33 +++++++++++---------------------- irc/connection.go | 11 +++++------ 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/irc/commands.go b/irc/commands.go index 3b74a7b..6fd9af8 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -3,10 +3,6 @@ package irc // this file contains the various commands you can // send to the server using an Conn connection -import ( - "reflect" -) - // This could be a lot less ugly with the ability to manipulate // the symbol table and add methods/functions on the fly // [ CMD, FMT, FMTARGS ] etc. @@ -30,7 +26,7 @@ func (conn *Conn) User(ident, name string) { func (conn *Conn) Join(channel string) { conn.out <- "JOIN "+channel } // Part() sends a PART command to the server with an optional part message -func (conn *Conn) Part(channel string, message ...) { +func (conn *Conn) Part(channel string, message ...string) { msg := getStringMsg(message) if msg != "" { msg = " :" + msg @@ -39,7 +35,7 @@ func (conn *Conn) Part(channel string, message ...) { } // Kick() sends a KICK command to remove a nick from a channel -func (conn *Conn) Kick(channel, nick string, message ...) { +func (conn *Conn) Kick(channel, nick string, message ...string) { msg := getStringMsg(message) if msg != "" { msg = " :" + msg @@ -48,7 +44,7 @@ func (conn *Conn) Kick(channel, nick string, message ...) { } // Quit() sends a QUIT command to the server with an optional quit message -func (conn *Conn) Quit(message ...) { +func (conn *Conn) Quit(message ...string) { msg := getStringMsg(message) if msg == "" { msg = "GoBye!" @@ -70,7 +66,7 @@ func (conn *Conn) Notice(t, msg string) { conn.out <- "NOTICE "+t+" :"+msg } // Ctcp() sends a (generic) CTCP message to the target t // with an optional argument -func (conn *Conn) Ctcp(t, ctcp string, arg ...) { +func (conn *Conn) Ctcp(t, ctcp string, arg ...string) { msg := getStringMsg(arg) if msg != "" { msg = " " + msg @@ -80,7 +76,7 @@ func (conn *Conn) Ctcp(t, ctcp string, arg ...) { // CtcpReply() sends a generic CTCP reply to the target t // with an optional argument -func (conn *Conn) CtcpReply(t, ctcp string, arg ...) { +func (conn *Conn) CtcpReply(t, ctcp string, arg ...string) { msg := getStringMsg(arg) if msg != "" { msg = " " + msg @@ -97,7 +93,7 @@ func (conn *Conn) Action(t, msg string) { conn.Ctcp(t, "ACTION", msg) } // Topic() sends a TOPIC command to the channel // Topic(channel) retrieves the current channel topic (see "332" handler) // Topic(channel, topic) sets the topic for the channel -func (conn *Conn) Topic(channel string, topic ...) { +func (conn *Conn) Topic(channel string, topic ...string) { t := getStringMsg(topic) if t != "" { t = " :" + t @@ -112,7 +108,7 @@ func (conn *Conn) Topic(channel string, topic ...) { // modestring == e.g. "+o " or "+ntk " or "-is" // This means you'll need to do your own mode work. It may be linked in with // the state tracking and ChanMode/NickMode/ChanPrivs objects later... -func (conn *Conn) Mode(t string, modestring ...) { +func (conn *Conn) Mode(t string, modestring ...string) { mode := getStringMsg(modestring) if mode != "" { mode = " " + mode @@ -123,7 +119,7 @@ func (conn *Conn) Mode(t string, modestring ...) { // Away() sends an AWAY command to the server // Away() resets away status // Away(message) sets away with the given message -func (conn *Conn) Away(message ...) { +func (conn *Conn) Away(message ...string) { msg := getStringMsg(message) if msg != "" { msg = " :"+msg @@ -141,16 +137,9 @@ func (conn *Conn) Oper(user, pass string) { conn.out <- "OPER "+user+" "+pass } -func getStringMsg(a ...) (msg string) { - // dealing with functions with a variable parameter list is nasteeh :-( - // the below stolen and munged from fmt/print.go func getString() - if v := reflect.NewValue(a).(*reflect.StructValue); v.NumField() > 0 { - if s, ok := v.Field(0).(*reflect.StringValue); ok { - return s.Get() - } - if b, ok := v.Interface().([]byte); ok { - return string(b) - } +func getStringMsg(a ...string) (msg string) { + if len(a) > 0 { + return a[0] } return "" } diff --git a/irc/connection.go b/irc/connection.go index c3f71dc..cd98da4 100644 --- a/irc/connection.go +++ b/irc/connection.go @@ -80,7 +80,7 @@ func (conn *Conn) initialise() { // Connect the IRC connection object to "host[:port]" which should be either // a hostname or an IP address, with an optional port defaulting to 6667. // You can also provide an optional connect password. -func (conn *Conn) Connect(host string, pass ...) os.Error { +func (conn *Conn) Connect(host string, pass ...string) os.Error { if conn.connected { return os.NewError(fmt.Sprintf("irc.Connect(): already connected to %s, cannot connect to %s", conn.Host, host)) } @@ -113,7 +113,7 @@ func (conn *Conn) Connect(host string, pass ...) os.Error { } // dispatch a nicely formatted os.Error to the error channel -func (conn *Conn) error(s string, a ...) { conn.Err <- os.NewError(fmt.Sprintf(s, a)) } +func (conn *Conn) error(s string, a ...interface{}) { conn.Err <- os.NewError(fmt.Sprintf(s, a)) } // copied from http.client for great justice func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") } @@ -142,7 +142,7 @@ func (conn *Conn) send() { // so sleep for the current line's time value before sending it time.Sleep(linetime) } - if err := conn.io.WriteString(line + "\r\n"); err != nil { + if _, err := conn.io.WriteString(line + "\r\n"); err != nil { conn.error("irc.send(): %s", err.String()) conn.shutdown() break @@ -161,8 +161,7 @@ func (conn *Conn) recv() { conn.shutdown() break } - // chop off \r\n - s = s[0 : len(s)-2] + s = strings.Trim(s, "\r\n") fmt.Println("<- " + s) line := &Line{Raw: s} @@ -193,7 +192,7 @@ func (conn *Conn) recv() { if len(args) > 1 { line.Text = args[1] } - args = strings.Split(args[0], " ", 0) + args = strings.Split(args[0], " ", -1) line.Cmd = strings.ToUpper(args[0]) if len(args) > 1 { line.Args = args[1:len(args)] From 36b764557718e3139aecedb5a18d8845826b4da2 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Sun, 29 Aug 2010 21:24:47 +0100 Subject: [PATCH 02/20] Makefile fixes to build with more recent Go releases. --- Makefile | 2 +- irc/Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 97bfc7a..b46474f 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. -include $(GOROOT)/src/Make.$(GOARCH) +include $(GOROOT)/src/Make.inc TARG=gobot GOFILES=\ diff --git a/irc/Makefile b/irc/Makefile index 813442c..e3f1bec 100644 --- a/irc/Makefile +++ b/irc/Makefile @@ -2,7 +2,7 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. -include $(GOROOT)/src/Make.$(GOARCH) +include $(GOROOT)/src/Make.inc TARG=irc GOFILES=\ From c4d09cd22889bd6020970f12e7ea37f00a922ac0 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Mon, 30 Aug 2010 12:03:01 +0100 Subject: [PATCH 03/20] Missed strings.Split() invocation; h/t jessta. --- irc/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irc/handlers.go b/irc/handlers.go index dec82fd..480bf9a 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -424,7 +424,7 @@ func (conn *Conn) setupEvents() { // Handle 353 names reply conn.AddHandler("353", func(conn *Conn, line *Line) { if ch := conn.GetChannel(line.Args[2]); ch != nil { - nicks := strings.Split(line.Text, " ", 0) + nicks := strings.Split(line.Text, " ", -1) for _, nick := range nicks { // UnrealIRCd's coders are lazy and leave a trailing space if nick == "" { From 05e3500a3c636cb2e0629e9412d766ab58582c17 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Mon, 30 Aug 2010 12:16:20 +0100 Subject: [PATCH 04/20] Enable SSL IRC for goirc. --- client.go | 4 ++-- irc/connection.go | 36 +++++++++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/client.go b/client.go index c2dc7e3..289971d 100644 --- a/client.go +++ b/client.go @@ -15,7 +15,7 @@ func main() { func(conn *irc.Conn, line *irc.Line) { conn.Join("#go-nuts") }) // connect to server - if err := c.Connect("irc.freenode.net", ""); err != nil { + if err := c.Connect("irc.freenode.net", false); err != nil { fmt.Printf("Connection error: %s\n", err) return } @@ -82,7 +82,7 @@ func main() { break } fmt.Println("Reconnecting...") - if err := c.Connect("irc.freenode.net", ""); err != nil { + if err := c.Connect("irc.freenode.net", false); err != nil { fmt.Printf("Connection error: %s\n", err) break } diff --git a/irc/connection.go b/irc/connection.go index cd98da4..42d7f70 100644 --- a/irc/connection.go +++ b/irc/connection.go @@ -4,6 +4,7 @@ import ( "bufio" "os" "net" + "crypto/tls" "fmt" "strings" "time" @@ -17,17 +18,20 @@ type Conn struct { Me *Nick // I/O stuff to server - sock *net.TCPConn + sock net.Conn io *bufio.ReadWriter in chan *Line out chan string connected bool + // Are we connecting via SSL? + SSL bool + // Error channel to transmit any fail back to the user Err chan os.Error // Set this to true to disable flood protection and false to re-enable - Flood bool; + Flood bool // Event handler mapping events map[string][]func(*Conn, *Line) @@ -78,22 +82,36 @@ func (conn *Conn) initialise() { } // Connect the IRC connection object to "host[:port]" which should be either -// a hostname or an IP address, with an optional port defaulting to 6667. -// You can also provide an optional connect password. -func (conn *Conn) Connect(host string, pass ...string) os.Error { +// a hostname or an IP address, with an optional port. To enable explicit SSL +// on the connection to the IRC server, set ssl to true. The port will default +// to 6697 if ssl is enabled, and 6667 otherwise. You can also provide an +// optional connect password. +func (conn *Conn) Connect(host string, ssl bool, pass ...string) os.Error { if conn.connected { return os.NewError(fmt.Sprintf("irc.Connect(): already connected to %s, cannot connect to %s", conn.Host, host)) } if !hasPort(host) { - host += ":6667" + if ssl { + host += ":6697" + } else { + host += ":6667" + } } - if addr, err := net.ResolveTCPAddr(host); err != nil { - return err - } else if conn.sock, err = net.DialTCP("tcp", nil, addr); err != nil { + var sock net.Conn; + var err os.Error; + if ssl { + sock, err = tls.Dial("tcp", "", host) + } else { + sock, err = net.Dial("tcp", "", host) + } + if err != nil { return err } + conn.Host = host + conn.SSL = ssl + conn.sock = sock conn.io = bufio.NewReadWriter( bufio.NewReader(conn.sock), From 7515f11470d72883f5852961c13618dc84b7ae47 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Sun, 24 Oct 2010 09:53:52 +0100 Subject: [PATCH 05/20] Use default root CAs for SSL connections, h/t raylu. --- irc/connection.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/irc/connection.go b/irc/connection.go index 42d7f70..3a88603 100644 --- a/irc/connection.go +++ b/irc/connection.go @@ -5,6 +5,7 @@ import ( "os" "net" "crypto/tls" + "crypto/rand" "fmt" "strings" "time" @@ -98,16 +99,13 @@ func (conn *Conn) Connect(host string, ssl bool, pass ...string) os.Error { } } - var sock net.Conn; - var err os.Error; - if ssl { - sock, err = tls.Dial("tcp", "", host) - } else { - sock, err = net.Dial("tcp", "", host) - } + sock, err := net.Dial("tcp", "", host) if err != nil { return err } + if ssl { + sock = tls.Client(sock, &tls.Config{Rand: rand.Reader, Time: time.Nanoseconds}) + } conn.Host = host conn.SSL = ssl @@ -119,9 +117,8 @@ func (conn *Conn) Connect(host string, ssl bool, pass ...string) os.Error { go conn.send() go conn.recv() - // see getStringMsg() in commands.go for what this does - if p := getStringMsg(pass); p != "" { - conn.Pass(p) + if pass != "" { + conn.Pass(pass) } conn.Nick(conn.Me.Nick) conn.User(conn.Me.Ident, conn.Me.Name) From 036cc4c3ebe0efbd05c2e65c2c59a81991f8ee81 Mon Sep 17 00:00:00 2001 From: raylu Date: Fri, 15 Oct 2010 18:06:51 -0400 Subject: [PATCH 06/20] Fix a bug that prevented adding multiple handlers --- irc/handlers.go | 1 + 1 file changed, 1 insertion(+) diff --git a/irc/handlers.go b/irc/handlers.go index 480bf9a..713ee12 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -34,6 +34,7 @@ func (conn *Conn) AddHandler(name string, f func(*Conn, *Line)) { } e = e[0 : len(e)+1] e[len(e)-1] = f + conn.events[n] = e } else { e := make([]func(*Conn, *Line), 1, 10) e[0] = f From 9b9197f5f6a2ac84ab4f57fe58a05297f305579c Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Sun, 24 Oct 2010 10:06:18 +0100 Subject: [PATCH 07/20] *cough* I should pay more attention when merging... --- irc/connection.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/irc/connection.go b/irc/connection.go index 3a88603..7a69d80 100644 --- a/irc/connection.go +++ b/irc/connection.go @@ -117,8 +117,8 @@ func (conn *Conn) Connect(host string, ssl bool, pass ...string) os.Error { go conn.send() go conn.recv() - if pass != "" { - conn.Pass(pass) + if len(pass) > 0 { + conn.Pass(pass[0]) } conn.Nick(conn.Me.Nick) conn.User(conn.Me.Ident, conn.Me.Name) From 34b3299d4153536344ea210ff4e7d9be9624485b Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Tue, 2 Nov 2010 21:47:05 +0000 Subject: [PATCH 08/20] Bring in Ray Lu's Debug patches. --- irc/connection.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/irc/connection.go b/irc/connection.go index 7a69d80..4b05f57 100644 --- a/irc/connection.go +++ b/irc/connection.go @@ -17,6 +17,7 @@ type Conn struct { // Connection Hostname and Nickname Host string Me *Nick + Network string // I/O stuff to server sock net.Conn @@ -34,6 +35,8 @@ type Conn struct { // Set this to true to disable flood protection and false to re-enable Flood bool + Debug bool + // Event handler mapping events map[string][]func(*Conn, *Line) @@ -163,7 +166,9 @@ func (conn *Conn) send() { break } conn.io.Flush() - fmt.Println("-> " + line) + if conn.Debug { + fmt.Println("-> " + line) + } } } @@ -177,7 +182,9 @@ func (conn *Conn) recv() { break } s = strings.Trim(s, "\r\n") - fmt.Println("<- " + s) + if conn.Debug { + fmt.Println("<- " + s) + } line := &Line{Raw: s} if s[0] == ':' { From 04db2e2c8d95e923c0d0ba4d26a2c32e06376dd8 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Wed, 3 Nov 2010 20:42:39 +0000 Subject: [PATCH 09/20] Revert "Use default root CAs for SSL connections, h/t raylu." This reverts commit 7515f11470d72883f5852961c13618dc84b7ae47. This doesn't use the "default" CAs, it explicitly turns off CA verification. Probably not the best of ideas. Patch to enable optional verification coming. Conflicts: irc/connection.go --- irc/connection.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/irc/connection.go b/irc/connection.go index 4b05f57..e30160b 100644 --- a/irc/connection.go +++ b/irc/connection.go @@ -5,7 +5,6 @@ import ( "os" "net" "crypto/tls" - "crypto/rand" "fmt" "strings" "time" @@ -102,13 +101,16 @@ func (conn *Conn) Connect(host string, ssl bool, pass ...string) os.Error { } } - sock, err := net.Dial("tcp", "", host) + var sock net.Conn; + var err os.Error; + if ssl { + sock, err = tls.Dial("tcp", "", host) + } else { + sock, err = net.Dial("tcp", "", host) + } if err != nil { return err } - if ssl { - sock = tls.Client(sock, &tls.Config{Rand: rand.Reader, Time: time.Nanoseconds}) - } conn.Host = host conn.SSL = ssl From f829eeab9b8c74b1d1762b165247f2bcceb5848b Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Wed, 3 Nov 2010 23:46:58 +0000 Subject: [PATCH 10/20] Merge in some other niceties from raylu. --- irc/connection.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/irc/connection.go b/irc/connection.go index e30160b..2e00a81 100644 --- a/irc/connection.go +++ b/irc/connection.go @@ -133,10 +133,14 @@ func (conn *Conn) Connect(host string, ssl bool, pass ...string) os.Error { } // dispatch a nicely formatted os.Error to the error channel -func (conn *Conn) error(s string, a ...interface{}) { conn.Err <- os.NewError(fmt.Sprintf(s, a)) } +func (conn *Conn) error(s string, a ...interface{}) { + conn.Err <- os.NewError(fmt.Sprintf(s, a...)) +} // copied from http.client for great justice -func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") } +func hasPort(s string) bool { + return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") +} // dispatch input from channel as \r\n terminated line to peer // flood controlled using hybrid's algorithm if conn.Flood is true @@ -216,7 +220,7 @@ func (conn *Conn) recv() { if len(args) > 1 { line.Text = args[1] } - args = strings.Split(args[0], " ", -1) + args = strings.Fields(args[0]) line.Cmd = strings.ToUpper(args[0]) if len(args) > 1 { line.Args = args[1:len(args)] From a9d47d1a2562191e848546e1c3d93cad77708431 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Wed, 3 Nov 2010 23:48:28 +0000 Subject: [PATCH 11/20] Sneakier handling of variadic optional args for commands. --- irc/commands.go | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/irc/commands.go b/irc/commands.go index 6fd9af8..8e2f589 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -1,5 +1,7 @@ package irc +import "strings" + // this file contains the various commands you can // send to the server using an Conn connection @@ -27,7 +29,7 @@ func (conn *Conn) Join(channel string) { conn.out <- "JOIN "+channel } // Part() sends a PART command to the server with an optional part message func (conn *Conn) Part(channel string, message ...string) { - msg := getStringMsg(message) + msg := strings.Join(message, " ") if msg != "" { msg = " :" + msg } @@ -36,7 +38,7 @@ func (conn *Conn) Part(channel string, message ...string) { // Kick() sends a KICK command to remove a nick from a channel func (conn *Conn) Kick(channel, nick string, message ...string) { - msg := getStringMsg(message) + msg := strings.Join(message, " ") if msg != "" { msg = " :" + msg } @@ -45,7 +47,7 @@ func (conn *Conn) Kick(channel, nick string, message ...string) { // Quit() sends a QUIT command to the server with an optional quit message func (conn *Conn) Quit(message ...string) { - msg := getStringMsg(message) + msg := strings.Join(message, " ") if msg == "" { msg = "GoBye!" } @@ -67,7 +69,7 @@ func (conn *Conn) Notice(t, msg string) { conn.out <- "NOTICE "+t+" :"+msg } // Ctcp() sends a (generic) CTCP message to the target t // with an optional argument func (conn *Conn) Ctcp(t, ctcp string, arg ...string) { - msg := getStringMsg(arg) + msg := strings.Join(arg, " ") if msg != "" { msg = " " + msg } @@ -77,7 +79,7 @@ func (conn *Conn) Ctcp(t, ctcp string, arg ...string) { // CtcpReply() sends a generic CTCP reply to the target t // with an optional argument func (conn *Conn) CtcpReply(t, ctcp string, arg ...string) { - msg := getStringMsg(arg) + msg := strings.Join(arg, " ") if msg != "" { msg = " " + msg } @@ -94,7 +96,7 @@ func (conn *Conn) Action(t, msg string) { conn.Ctcp(t, "ACTION", msg) } // Topic(channel) retrieves the current channel topic (see "332" handler) // Topic(channel, topic) sets the topic for the channel func (conn *Conn) Topic(channel string, topic ...string) { - t := getStringMsg(topic) + t := strings.Join(topic, " ") if t != "" { t = " :" + t } @@ -109,7 +111,7 @@ func (conn *Conn) Topic(channel string, topic ...string) { // This means you'll need to do your own mode work. It may be linked in with // the state tracking and ChanMode/NickMode/ChanPrivs objects later... func (conn *Conn) Mode(t string, modestring ...string) { - mode := getStringMsg(modestring) + mode := strings.Join(modestring, " ") if mode != "" { mode = " " + mode } @@ -120,7 +122,7 @@ func (conn *Conn) Mode(t string, modestring ...string) { // Away() resets away status // Away(message) sets away with the given message func (conn *Conn) Away(message ...string) { - msg := getStringMsg(message) + msg := strings.Join(message, " ") if msg != "" { msg = " :"+msg } @@ -136,10 +138,3 @@ func (conn *Conn) Invite(nick, channel string) { func (conn *Conn) Oper(user, pass string) { conn.out <- "OPER "+user+" "+pass } - -func getStringMsg(a ...string) (msg string) { - if len(a) > 0 { - return a[0] - } - return "" -} From 38eb150850f78c98d929a91414875c902424471d Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Wed, 3 Nov 2010 23:49:28 +0000 Subject: [PATCH 12/20] Somewhat better SSL handling. It'll improve moar if my patches get looked at. --- irc/connection.go | 58 +++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/irc/connection.go b/irc/connection.go index 2e00a81..35502b1 100644 --- a/irc/connection.go +++ b/irc/connection.go @@ -25,8 +25,9 @@ type Conn struct { out chan string connected bool - // Are we connecting via SSL? + // Are we connecting via SSL? Do we care about certificate validity? SSL bool + SSLConfig *tls.Config // Error channel to transmit any fail back to the user Err chan os.Error @@ -74,6 +75,8 @@ func (conn *Conn) initialise() { conn.in = make(chan *Line, 32) conn.out = make(chan string, 32) conn.Err = make(chan os.Error, 4) + conn.SSL = false + conn.SSLConfig = nil conn.io = nil conn.sock = nil @@ -86,36 +89,47 @@ func (conn *Conn) initialise() { // Connect the IRC connection object to "host[:port]" which should be either // a hostname or an IP address, with an optional port. To enable explicit SSL -// on the connection to the IRC server, set ssl to true. The port will default -// to 6697 if ssl is enabled, and 6667 otherwise. You can also provide an -// optional connect password. -func (conn *Conn) Connect(host string, ssl bool, pass ...string) os.Error { +// on the connection to the IRC server, set Conn.SSL to true before calling +// Connect(). The port will default to 6697 if ssl is enabled, and 6667 +// otherwise. You can also provide an optional connect password. +func (conn *Conn) Connect(host string, pass ...string) os.Error { if conn.connected { - return os.NewError(fmt.Sprintf("irc.Connect(): already connected to %s, cannot connect to %s", conn.Host, host)) + return os.NewError(fmt.Sprintf( + "irc.Connect(): already connected to %s, cannot connect to %s", + conn.Host, host)) } - if !hasPort(host) { - if ssl { + + if conn.SSL { + if !hasPort(host) { host += ":6697" + } + // It's unfortunate that tls.Dial doesn't allow a tls.Config arg, + // so we simply replicate it here with the correct Config. + // http://codereview.appspot.com/2883041 + if s, err := net.Dial("tcp", "", host); err == nil { + // Passing nil config => certs are validated. + c := tls.Client(s, conn.SSLConfig) + if err = c.Handshake(); err == nil { + conn.sock = c + } else { + s.Close() + return err + } } else { + return err + } + } else { + if !hasPort(host) { host += ":6667" } + if s, err := net.Dial("tcp", "", host); err == nil { + conn.sock = s + } else { + return err + } } - var sock net.Conn; - var err os.Error; - if ssl { - sock, err = tls.Dial("tcp", "", host) - } else { - sock, err = net.Dial("tcp", "", host) - } - if err != nil { - return err - } - conn.Host = host - conn.SSL = ssl - conn.sock = sock - conn.io = bufio.NewReadWriter( bufio.NewReader(conn.sock), bufio.NewWriter(conn.sock)) From 7935d2e9390962538d017faebbd48fb498ad7447 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Wed, 3 Nov 2010 23:50:43 +0000 Subject: [PATCH 13/20] Use append() instead of doing it manually. --- irc/handlers.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/irc/handlers.go b/irc/handlers.go index 713ee12..cb60055 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -24,17 +24,7 @@ import ( func (conn *Conn) AddHandler(name string, f func(*Conn, *Line)) { n := strings.ToUpper(name) if e, ok := conn.events[n]; ok { - if len(e) == cap(e) { - // crap, we're full. expand e by another 10 handler slots - ne := make([]func(*Conn, *Line), len(e), len(e)+10) - for i := 0; i < len(e); i++ { - ne[i] = e[i] - } - e = ne - } - e = e[0 : len(e)+1] - e[len(e)-1] = f - conn.events[n] = e + conn.events[n] = append(e, f) } else { e := make([]func(*Conn, *Line), 1, 10) e[0] = f From 961e38d7bf2b135e1150fd7491ec303fb7ca89f0 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Wed, 3 Nov 2010 23:51:36 +0000 Subject: [PATCH 14/20] Handle ircu's non-conformist attitude to JOIN/PART formatting(h/t raylu again). --- irc/handlers.go | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/irc/handlers.go b/irc/handlers.go index cb60055..5e91d65 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -146,7 +146,16 @@ func (conn *Conn) setupEvents() { // Handle JOINs to channels to maintain state conn.AddHandler("JOIN", func(conn *Conn, line *Line) { - ch := conn.GetChannel(line.Text) + // Some IRCds (ircu) send ':n!u@h JOIN #chan' not ':n!u@h JOIN :#chan' + // Unfortunately the RFCs aren't specific about this. In fact the + // examples indicate no colon should be sent, but it's unusual. + var chname string + if len(line.Text) > 0 { + chname = line.Text + } else if len(line.Args) > 0 { + chname = line.Args[0] + } + ch := conn.GetChannel(chname) n := conn.GetNick(line.Nick) if ch == nil { // first we've seen of this channel, so should be us joining it @@ -155,7 +164,7 @@ func (conn *Conn) setupEvents() { conn.error("irc.JOIN(): buh? JOIN to unknown channel %s recieved from (non-me) nick %s", line.Text, line.Nick) return } - ch = conn.NewChannel(line.Text) + ch = conn.NewChannel(chname) // since we don't know much about this channel, ask server for info // we get the channel users automatically in 353 and the channel // topic in 332 on join, so we just need to get the modes @@ -176,12 +185,20 @@ func (conn *Conn) setupEvents() { // Handle PARTs from channels to maintain state conn.AddHandler("PART", func(conn *Conn, line *Line) { - ch := conn.GetChannel(line.Args[0]) + // Some IRCds (ircu) send 'PART :#chan' when there's no part message + // instead of 'PART #chan'. This is *questionable* behaviour... + var chname string + if len(line.Args) > 0 { + chname = line.Args[0] + } else if len(line.Text) > 0 { + chname = line.Text + } + ch := conn.GetChannel(chname) n := conn.GetNick(line.Nick) if ch != nil && n != nil { ch.DelNick(n) } else { - conn.error("irc.PART(): buh? PART of channel %s by nick %s", line.Args[0], line.Nick) + conn.error("irc.PART(): buh? PART of channel %s by nick %s", chname, line.Nick) } }) From 419dba1e7512495a07f9111ce08aa68203909883 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Wed, 3 Nov 2010 23:53:14 +0000 Subject: [PATCH 15/20] Update example client with changes. --- client.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 289971d..b747d29 100644 --- a/client.go +++ b/client.go @@ -11,11 +11,12 @@ import ( func main() { // create new IRC connection c := irc.New("GoTest", "gotest", "GoBot") + c.Debug = true c.AddHandler("connected", func(conn *irc.Conn, line *irc.Line) { conn.Join("#go-nuts") }) // connect to server - if err := c.Connect("irc.freenode.net", false); err != nil { + if err := c.Connect("irc.freenode.net"); err != nil { fmt.Printf("Connection error: %s\n", err) return } @@ -82,7 +83,7 @@ func main() { break } fmt.Println("Reconnecting...") - if err := c.Connect("irc.freenode.net", false); err != nil { + if err := c.Connect("irc.freenode.net"); err != nil { fmt.Printf("Connection error: %s\n", err) break } From ff61bc9ea10dd77ddc19b1955bfa119df598b846 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Thu, 4 Nov 2010 00:02:26 +0000 Subject: [PATCH 16/20] Update README with slight changes. --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7ef8050..ecf9029 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,12 @@ Synopsis: import "irc" func main() { c := irc.New("nick", "ident", "real name") + // Optionally, turn on debugging + c.Debug = true + // Optionally, enable SSL + c.SSL = true // add handlers to do things here! - if err := c.Connect("irc.freenode.net", ""); err != nil { + if err := c.Connect("irc.freenode.net"); err != nil { fmt.Printf("Connection error: %s\n", err.String()) } for { @@ -58,5 +62,5 @@ indebted to Matt Gruen for his work on the re-organisation and channel-based communication structure of `*Conn.send()` and `*Conn.recv()`. I'm sure things could be more asynchronous, still. -This code is (c) 2009 Alex Bramley, and released under the same licence terms +This code is (c) 2009-10 Alex Bramley, and released under the same licence terms as Go itself. From 6bb5558c46dd3b633422bd11dc1da2d31f4e73c8 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Thu, 4 Nov 2010 00:06:27 +0000 Subject: [PATCH 17/20] Bugfix -- there could be other integer modes than +l. --- irc/nickchan.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irc/nickchan.go b/irc/nickchan.go index a050373..382862c 100644 --- a/irc/nickchan.go +++ b/irc/nickchan.go @@ -326,7 +326,7 @@ func (cm *ChanMode) String() string { case *reflect.IntValue: if f.Get() != 0 { str += ChanModeToString[t.Field(i).Name] - a[1] = fmt.Sprintf("%d", cm.Limit) + a[1] = fmt.Sprintf("%d", f.Get()) } } } From c419ef1eadb33237d89963a0d2681385c5e43ccc Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Thu, 4 Nov 2010 00:25:46 +0000 Subject: [PATCH 18/20] Run gofmt over everything, for great justice. --- irc/commands.go | 2 +- irc/connection.go | 14 +++++++------- irc/handlers.go | 6 +++--- irc/irc_test.go | 1 - irc/nickchan.go | 42 +++++++++++++++++++++--------------------- 5 files changed, 32 insertions(+), 33 deletions(-) diff --git a/irc/commands.go b/irc/commands.go index 8e2f589..5041bdd 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -124,7 +124,7 @@ func (conn *Conn) Mode(t string, modestring ...string) { func (conn *Conn) Away(message ...string) { msg := strings.Join(message, " ") if msg != "" { - msg = " :"+msg + msg = " :" + msg } conn.out <- "AWAY"+msg } diff --git a/irc/connection.go b/irc/connection.go index 35502b1..03669b7 100644 --- a/irc/connection.go +++ b/irc/connection.go @@ -14,8 +14,8 @@ import ( // encountered are piped down *Conn.Err; this channel is closed on disconnect. type Conn struct { // Connection Hostname and Nickname - Host string - Me *Nick + Host string + Me *Nick Network string // I/O stuff to server @@ -26,7 +26,7 @@ type Conn struct { connected bool // Are we connecting via SSL? Do we care about certificate validity? - SSL bool + SSL bool SSLConfig *tls.Config // Error channel to transmit any fail back to the user @@ -95,8 +95,8 @@ func (conn *Conn) initialise() { func (conn *Conn) Connect(host string, pass ...string) os.Error { if conn.connected { return os.NewError(fmt.Sprintf( - "irc.Connect(): already connected to %s, cannot connect to %s", - conn.Host, host)) + "irc.Connect(): already connected to %s, cannot connect to %s", + conn.Host, host)) } if conn.SSL { @@ -160,7 +160,7 @@ func hasPort(s string) bool { // flood controlled using hybrid's algorithm if conn.Flood is true func (conn *Conn) send() { lastsent := time.Nanoseconds() - var badness, linetime, second int64 = 0, 0, 1000000000; + var badness, linetime, second int64 = 0, 0, 1000000000 for line := range conn.out { // Hybrid's algorithm allows for 2 seconds per line and an additional // 1/120 of a second per character on that line. @@ -245,7 +245,7 @@ func (conn *Conn) recv() { func (conn *Conn) runLoop() { for line := range conn.in { - conn.dispatchEvent(line) + conn.dispatchEvent(line) } } diff --git a/irc/handlers.go b/irc/handlers.go index 5e91d65..aac3c2e 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -108,9 +108,9 @@ func (conn *Conn) setupEvents() { // XXX: do we need 005 protocol support message parsing here? // probably in the future, but I can't quite be arsed yet. /* - :irc.pl0rt.org 005 GoTest CMDS=KNOCK,MAP,DCCALLOW,USERIP UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=20 CHANLIMIT=#:20 MAXLIST=b:60,e:60,I:60 NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 :are supported by this server - :irc.pl0rt.org 005 GoTest MAXTARGETS=20 WALLCHOPS WATCH=128 WATCHOPTS=A SILENCE=15 MODES=12 CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beI,kfL,lj,psmntirRcOAQKVCuzNSMT NETWORK=bb101.net CASEMAPPING=ascii EXTBAN=~,cqnr ELIST=MNUCT :are supported by this server - :irc.pl0rt.org 005 GoTest STATUSMSG=~&@%+ EXCEPTS INVEX :are supported by this server + :irc.pl0rt.org 005 GoTest CMDS=KNOCK,MAP,DCCALLOW,USERIP UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=20 CHANLIMIT=#:20 MAXLIST=b:60,e:60,I:60 NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 :are supported by this server + :irc.pl0rt.org 005 GoTest MAXTARGETS=20 WALLCHOPS WATCH=128 WATCHOPTS=A SILENCE=15 MODES=12 CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beI,kfL,lj,psmntirRcOAQKVCuzNSMT NETWORK=bb101.net CASEMAPPING=ascii EXTBAN=~,cqnr ELIST=MNUCT :are supported by this server + :irc.pl0rt.org 005 GoTest STATUSMSG=~&@%+ EXCEPTS INVEX :are supported by this server */ // Handler to deal with "433 :Nickname already in use" diff --git a/irc/irc_test.go b/irc/irc_test.go index eba9ca6..68ade89 100644 --- a/irc/irc_test.go +++ b/irc/irc_test.go @@ -12,4 +12,3 @@ func TestIRC(t *testing.T) { t.FailNow() } } - diff --git a/irc/nickchan.go b/irc/nickchan.go index 382862c..04b182c 100644 --- a/irc/nickchan.go +++ b/irc/nickchan.go @@ -203,44 +203,44 @@ func (n *Nick) Delete() { // Map *irc.ChanMode fields to IRC mode characters var ChanModeToString = map[string]string{ - "Private": "p", - "Secret": "s", + "Private": "p", + "Secret": "s", "ProtectedTopic": "t", - "NoExternalMsg": "n", - "Moderated": "m", - "InviteOnly": "i", - "OperOnly": "O", - "SSLOnly": "z", - "Key": "k", - "Limit": "l", + "NoExternalMsg": "n", + "Moderated": "m", + "InviteOnly": "i", + "OperOnly": "O", + "SSLOnly": "z", + "Key": "k", + "Limit": "l", } // Map *irc.NickMode fields to IRC mode characters var NickModeToString = map[string]string{ - "Invisible": "i", - "Oper": "o", - "WallOps": "w", + "Invisible": "i", + "Oper": "o", + "WallOps": "w", "HiddenHost": "x", - "SSL": "z", + "SSL": "z", } // Map *irc.ChanPrivs fields to IRC mode characters var ChanPrivToString = map[string]string{ - "Owner": "q", - "Admin": "a", - "Op": "o", + "Owner": "q", + "Admin": "a", + "Op": "o", "HalfOp": "h", - "Voice": "v", + "Voice": "v", } // Map *irc.ChanPrivs fields to the symbols used to represent these modes // in NAMES and WHOIS responses var ChanPrivToModeChar = map[string]byte{ - "Owner": '~', - "Admin": '&', - "Op": '@', + "Owner": '~', + "Admin": '&', + "Op": '@', "HalfOp": '%', - "Voice": '+', + "Voice": '+', } // Reverse mappings of the above datastructures From e611672b06df5bde7bce40e9ae326e6ea912f7b8 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Thu, 4 Nov 2010 00:54:26 +0000 Subject: [PATCH 19/20] Reorganise handlers into separate methods rather than anonymous functions. --- irc/handlers.go | 814 ++++++++++++++++++++++++------------------------ 1 file changed, 413 insertions(+), 401 deletions(-) diff --git a/irc/handlers.go b/irc/handlers.go index aac3c2e..c1c35cb 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -75,410 +75,422 @@ func (conn *Conn) dispatchEvent(line *Line) { } } +// Basic ping/pong handler +func (conn *Conn) h_PING(line *Line) { + conn.Raw("PONG :" + line.Text) +} + +// Handler to trigger a "CONNECTED" event on receipt of numeric 001 +func (conn *Conn) h_001(line *Line) { + // we're connected! + conn.connected = true + conn.dispatchEvent(&Line{Cmd: "CONNECTED"}) + // and we're being given our hostname (from the server's perspective) + if ridx := strings.LastIndex(line.Text, " "); ridx != -1 { + h := line.Text[ridx+1 : len(line.Text)] + if idx := strings.Index(h, "@"); idx != -1 { + conn.Me.Host = h[idx+1 : len(h)] + } + } +} + +// XXX: do we need 005 protocol support message parsing here? +// probably in the future, but I can't quite be arsed yet. +/* + :irc.pl0rt.org 005 GoTest CMDS=KNOCK,MAP,DCCALLOW,USERIP UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=20 CHANLIMIT=#:20 MAXLIST=b:60,e:60,I:60 NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 :are supported by this server + :irc.pl0rt.org 005 GoTest MAXTARGETS=20 WALLCHOPS WATCH=128 WATCHOPTS=A SILENCE=15 MODES=12 CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beI,kfL,lj,psmntirRcOAQKVCuzNSMT NETWORK=bb101.net CASEMAPPING=ascii EXTBAN=~,cqnr ELIST=MNUCT :are supported by this server + :irc.pl0rt.org 005 GoTest STATUSMSG=~&@%+ EXCEPTS INVEX :are supported by this server +*/ + +// Handler to deal with "433 :Nickname already in use" +func (conn *Conn) h_433(line *Line) { + // Args[1] is the new nick we were attempting to acquire + conn.Nick(line.Args[1] + "_") + // if this is happening before we're properly connected (i.e. the nick + // we sent in the initial NICK command is in use) we will not receive + // a NICK message to confirm our change of nick, so ReNick here... + if !conn.connected && line.Args[1] == conn.Me.Nick { + conn.Me.ReNick(line.Args[1] + "_") + } +} + +// Handler NICK messages to inform us about nick changes +func (conn *Conn) h_NICK(line *Line) { + // all nicks should be handled the same way, our own included + if n := conn.GetNick(line.Nick); n != nil { + n.ReNick(line.Text) + } else { + conn.error("irc.NICK(): buh? unknown nick %s.", line.Nick) + } +} + +// Handle VERSION requests and CTCP PING +func (conn *Conn) h_CTCP(line *Line) { + if line.Args[0] == "VERSION" { + conn.CtcpReply(line.Nick, "VERSION", "powered by goirc...") + } else if line.Args[0] == "PING" { + conn.CtcpReply(line.Nick, "PING", line.Text) + } +} + +// Handle JOINs to channels to maintain state +func (conn *Conn) h_JOIN(line *Line) { + // Some IRCds (ircu) send ':n!u@h JOIN #chan' not ':n!u@h JOIN :#chan' + // Unfortunately the RFCs aren't specific about this. In fact the + // examples indicate no colon should be sent, but it's unusual. + var chname string + if len(line.Text) > 0 { + chname = line.Text + } else if len(line.Args) > 0 { + chname = line.Args[0] + } + ch := conn.GetChannel(chname) + n := conn.GetNick(line.Nick) + if ch == nil { + // first we've seen of this channel, so should be us joining it + // NOTE this will also take care of n == nil && ch == nil + if n != conn.Me { + conn.error("irc.JOIN(): buh? JOIN to unknown channel %s recieved from (non-me) nick %s", line.Text, line.Nick) + return + } + ch = conn.NewChannel(chname) + // since we don't know much about this channel, ask server for info + // we get the channel users automatically in 353 and the channel + // topic in 332 on join, so we just need to get the modes + conn.Mode(ch.Name) + // sending a WHO for the channel is MUCH more efficient than + // triggering a WHOIS on every nick from the 353 handler + conn.Who(ch.Name) + } + if n == nil { + // this is the first we've seen of this nick + n = conn.NewNick(line.Nick, line.Ident, "", line.Host) + // since we don't know much about this nick, ask server for info + conn.Who(n.Nick) + } + // this takes care of both nick and channel linking \o/ + ch.AddNick(n) +} + +// Handle PARTs from channels to maintain state +func (conn *Conn) h_PART(line *Line) { + // Some IRCds (ircu) send 'PART :#chan' when there's no part message + // instead of 'PART #chan'. This is *questionable* behaviour... + var chname string + if len(line.Args) > 0 { + chname = line.Args[0] + } else if len(line.Text) > 0 { + chname = line.Text + } + ch := conn.GetChannel(chname) + n := conn.GetNick(line.Nick) + if ch != nil && n != nil { + ch.DelNick(n) + } else { + conn.error("irc.PART(): buh? PART of channel %s by nick %s", chname, line.Nick) + } +} + +// Handle KICKs from channels to maintain state +func (conn *Conn) h_KICK(line *Line) { + // XXX: this won't handle autorejoining channels on KICK + // it's trivial to do this in a seperate handler... + ch := conn.GetChannel(line.Args[0]) + n := conn.GetNick(line.Args[1]) + if ch != nil && n != nil { + ch.DelNick(n) + } else { + conn.error("irc.KICK(): buh? KICK from channel %s of nick %s", line.Args[0], line.Args[1]) + } +} + +// Handle other people's QUITs +func (conn *Conn) h_QUIT(line *Line) { + if n := conn.GetNick(line.Nick); n != nil { + n.Delete() + } else { + conn.error("irc.QUIT(): buh? QUIT from unknown nick %s", line.Nick) + } +} + +// Handle MODE changes for channels we know about (and our nick personally) +// this is moderately ugly. suggestions for improvement welcome +func (conn *Conn) h_MODE(line *Line) { + // channel modes first + if ch := conn.GetChannel(line.Args[0]); ch != nil { + modeargs := line.Args[2:len(line.Args)] + var modeop bool // true => add mode, false => remove mode + var modestr string + for i := 0; i < len(line.Args[1]); i++ { + switch m := line.Args[1][i]; m { + case '+': + modeop = true + modestr = string(m) + case '-': + modeop = false + modestr = string(m) + case 'i': + ch.Modes.InviteOnly = modeop + case 'm': + ch.Modes.Moderated = modeop + case 'n': + ch.Modes.NoExternalMsg = modeop + case 'p': + ch.Modes.Private = modeop + case 's': + ch.Modes.Secret = modeop + case 't': + ch.Modes.ProtectedTopic = modeop + case 'z': + ch.Modes.SSLOnly = modeop + case 'O': + ch.Modes.OperOnly = modeop + case 'k': + if len(modeargs) != 0 { + ch.Modes.Key, modeargs = modeargs[0], modeargs[1:len(modeargs)] + } else { + conn.error("irc.MODE(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) + } + case 'l': + if len(modeargs) != 0 { + ch.Modes.Limit, _ = strconv.Atoi(modeargs[0]) + modeargs = modeargs[1:len(modeargs)] + } else { + conn.error("irc.MODE(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) + } + case 'q', 'a', 'o', 'h', 'v': + if len(modeargs) != 0 { + n := conn.GetNick(modeargs[0]) + if p, ok := ch.Nicks[n]; ok && n != nil { + switch m { + case 'q': + p.Owner = modeop + case 'a': + p.Admin = modeop + case 'o': + p.Op = modeop + case 'h': + p.HalfOp = modeop + case 'v': + p.Voice = modeop + } + modeargs = modeargs[1:len(modeargs)] + } else { + conn.error("irc.MODE(): MODE %s %s%s %s: buh? state tracking failure.", ch.Name, modestr, m, modeargs[0]) + } + } else { + conn.error("irc.MODE(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) + } + } + } + } else if n := conn.GetNick(line.Args[0]); n != nil { + // nick mode change, should be us + if n != conn.Me { + conn.error("irc.MODE(): buh? recieved MODE %s for (non-me) nick %s", line.Text, n.Nick) + return + } + var modeop bool // true => add mode, false => remove mode + for i := 0; i < len(line.Text); i++ { + switch m := line.Text[i]; m { + case '+': + modeop = true + case '-': + modeop = false + case 'i': + n.Modes.Invisible = modeop + case 'o': + n.Modes.Oper = modeop + case 'w': + n.Modes.WallOps = modeop + case 'x': + n.Modes.HiddenHost = modeop + case 'z': + n.Modes.SSL = modeop + } + } + } else { + if line.Text != "" { + conn.error("irc.MODE(): buh? not sure what to do with nick MODE %s %s", line.Args[0], line.Text) + } else { + conn.error("irc.MODE(): buh? not sure what to do with chan MODE %s", strings.Join(line.Args, " ")) + } + } +} + +// Handle TOPIC changes for channels +func (conn *Conn) h_TOPIC(line *Line) { + if ch := conn.GetChannel(line.Args[0]); ch != nil { + ch.Topic = line.Text + } else { + conn.error("irc.TOPIC(): buh? topic change on unknown channel %s", line.Args[0]) + } +} + +// Handle 311 whois reply +func (conn *Conn) h_311(line *Line) { + if n := conn.GetNick(line.Args[1]); n != nil { + n.Ident = line.Args[2] + n.Host = line.Args[3] + n.Name = line.Text + } else { + conn.error("irc.311(): buh? received WHOIS info for unknown nick %s", line.Args[1]) + } +} + +// Handle 324 mode reply +func (conn *Conn) h_324(line *Line) { + // XXX: copypasta from MODE, needs tidying. + if ch := conn.GetChannel(line.Args[1]); ch != nil { + modeargs := line.Args[3:len(line.Args)] + var modeop bool // true => add mode, false => remove mode + var modestr string + for i := 0; i < len(line.Args[2]); i++ { + switch m := line.Args[2][i]; m { + case '+': + modeop = true + modestr = string(m) + case '-': + modeop = false + modestr = string(m) + case 'i': + ch.Modes.InviteOnly = modeop + case 'm': + ch.Modes.Moderated = modeop + case 'n': + ch.Modes.NoExternalMsg = modeop + case 'p': + ch.Modes.Private = modeop + case 's': + ch.Modes.Secret = modeop + case 't': + ch.Modes.ProtectedTopic = modeop + case 'z': + ch.Modes.SSLOnly = modeop + case 'O': + ch.Modes.OperOnly = modeop + case 'k': + if len(modeargs) != 0 { + ch.Modes.Key, modeargs = modeargs[0], modeargs[1:len(modeargs)] + } else { + conn.error("irc.324(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) + } + case 'l': + if len(modeargs) != 0 { + ch.Modes.Limit, _ = strconv.Atoi(modeargs[0]) + modeargs = modeargs[1:len(modeargs)] + } else { + conn.error("irc.324(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) + } + } + } + } else { + conn.error("irc.324(): buh? received MODE settings for unknown channel %s", line.Args[1]) + } +} + +// Handle 332 topic reply on join to channel +func (conn *Conn) h_332(line *Line) { + if ch := conn.GetChannel(line.Args[1]); ch != nil { + ch.Topic = line.Text + } else { + conn.error("irc.332(): buh? received TOPIC value for unknown channel %s", line.Args[1]) + } +} + +// Handle 352 who reply +func (conn *Conn) h_352(line *Line) { + if n := conn.GetNick(line.Args[5]); n != nil { + n.Ident = line.Args[2] + n.Host = line.Args[3] + // XXX: do we care about the actual server the nick is on? + // or the hop count to this server? + // line.Text contains " " + a := strings.Split(line.Text, " ", 2) + n.Name = a[1] + if idx := strings.Index(line.Args[6], "*"); idx != -1 { + n.Modes.Oper = true + } + if idx := strings.Index(line.Args[6], "H"); idx != -1 { + n.Modes.Invisible = true + } + } else { + conn.error("irc.352(): buh? got WHO reply for unknown nick %s", line.Args[5]) + } +} + +// Handle 353 names reply +func (conn *Conn) h_353(line *Line) { + if ch := conn.GetChannel(line.Args[2]); ch != nil { + nicks := strings.Split(line.Text, " ", -1) + for _, nick := range nicks { + // UnrealIRCd's coders are lazy and leave a trailing space + if nick == "" { + continue + } + switch c := nick[0]; c { + case '~', '&', '@', '%', '+': + nick = nick[1:len(nick)] + fallthrough + default: + n := conn.GetNick(nick) + if n == nil { + // we don't know this nick yet! + n = conn.NewNick(nick, "", "", "") + } + if n != conn.Me { + // we will be in the names list, but should also be in + // the channel's nick list from the JOIN handler above + ch.AddNick(n) + } + p := ch.Nicks[n] + switch c { + case '~': + p.Owner = true + case '&': + p.Admin = true + case '@': + p.Op = true + case '%': + p.HalfOp = true + case '+': + p.Voice = true + } + } + } + } else { + conn.error("irc.353(): buh? received NAMES list for unknown channel %s", line.Args[2]) + } +} + +// Handle 671 whois reply (nick connected via SSL) +func (conn *Conn) h_671(line *Line) { + if n := conn.GetNick(line.Args[1]); n != nil { + n.Modes.SSL = true + } else { + conn.error("irc.671(): buh? received WHOIS SSL info for unknown nick %s", line.Args[1]) + } +} + // sets up the internal event handlers to do useful things with lines -// XXX: is there a better way of doing this? -// Turns out there may be but it's not actually implemented in the language yet -// according to the people on freenode/#go-nuts ... :-( -// see: http://golang.org/doc/go_spec.html#Method_expressions for details -// I think this means we should be able to do something along the lines of: -// conn.AddHandler("event", (*Conn).h_handler); -// where h_handler is declared in the irc package as: -// func (conn *Conn) h_handler(line *Line) {} -// in the future, but for now the compiler throws a hissy fit. func (conn *Conn) setupEvents() { conn.events = make(map[string][]func(*Conn, *Line)) - // Basic ping/pong handler - conn.AddHandler("PING", func(conn *Conn, line *Line) { conn.Raw("PONG :" + line.Text) }) + conn.AddHandler("CTCP", (*Conn).h_CTCP) + conn.AddHandler("JOIN", (*Conn).h_JOIN) + conn.AddHandler("KICK", (*Conn).h_KICK) + conn.AddHandler("MODE", (*Conn).h_MODE) + conn.AddHandler("NICK", (*Conn).h_NICK) + conn.AddHandler("PART", (*Conn).h_PART) + conn.AddHandler("PING", (*Conn).h_PING) + conn.AddHandler("QUIT", (*Conn).h_QUIT) + conn.AddHandler("TOPIC", (*Conn).h_TOPIC) - // Handler to trigger a "CONNECTED" event on receipt of numeric 001 - conn.AddHandler("001", func(conn *Conn, line *Line) { - // we're connected! - conn.connected = true - conn.dispatchEvent(&Line{Cmd: "CONNECTED"}) - // and we're being given our hostname (from the server's perspective) - if ridx := strings.LastIndex(line.Text, " "); ridx != -1 { - h := line.Text[ridx+1 : len(line.Text)] - if idx := strings.Index(h, "@"); idx != -1 { - conn.Me.Host = h[idx+1 : len(h)] - } - } - }) - - // XXX: do we need 005 protocol support message parsing here? - // probably in the future, but I can't quite be arsed yet. - /* - :irc.pl0rt.org 005 GoTest CMDS=KNOCK,MAP,DCCALLOW,USERIP UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=20 CHANLIMIT=#:20 MAXLIST=b:60,e:60,I:60 NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 :are supported by this server - :irc.pl0rt.org 005 GoTest MAXTARGETS=20 WALLCHOPS WATCH=128 WATCHOPTS=A SILENCE=15 MODES=12 CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beI,kfL,lj,psmntirRcOAQKVCuzNSMT NETWORK=bb101.net CASEMAPPING=ascii EXTBAN=~,cqnr ELIST=MNUCT :are supported by this server - :irc.pl0rt.org 005 GoTest STATUSMSG=~&@%+ EXCEPTS INVEX :are supported by this server - */ - - // Handler to deal with "433 :Nickname already in use" - conn.AddHandler("433", func(conn *Conn, line *Line) { - // Args[1] is the new nick we were attempting to acquire - conn.Nick(line.Args[1] + "_") - // if this is happening before we're properly connected (i.e. the nick - // we sent in the initial NICK command is in use) we will not receive - // a NICK message to confirm our change of nick, so ReNick here... - if !conn.connected && line.Args[1] == conn.Me.Nick { - conn.Me.ReNick(line.Args[1] + "_") - } - }) - - // Handler NICK messages to inform us about nick changes - conn.AddHandler("NICK", func(conn *Conn, line *Line) { - // all nicks should be handled the same way, our own included - if n := conn.GetNick(line.Nick); n != nil { - n.ReNick(line.Text) - } else { - conn.error("irc.NICK(): buh? unknown nick %s.", line.Nick) - } - }) - - // Handle VERSION requests and CTCP PING - conn.AddHandler("CTCP", func(conn *Conn, line *Line) { - if line.Args[0] == "VERSION" { - conn.CtcpReply(line.Nick, "VERSION", "powered by goirc...") - } else if line.Args[0] == "PING" { - conn.CtcpReply(line.Nick, "PING", line.Text) - } - }) - - // Handle JOINs to channels to maintain state - conn.AddHandler("JOIN", func(conn *Conn, line *Line) { - // Some IRCds (ircu) send ':n!u@h JOIN #chan' not ':n!u@h JOIN :#chan' - // Unfortunately the RFCs aren't specific about this. In fact the - // examples indicate no colon should be sent, but it's unusual. - var chname string - if len(line.Text) > 0 { - chname = line.Text - } else if len(line.Args) > 0 { - chname = line.Args[0] - } - ch := conn.GetChannel(chname) - n := conn.GetNick(line.Nick) - if ch == nil { - // first we've seen of this channel, so should be us joining it - // NOTE this will also take care of n == nil && ch == nil - if n != conn.Me { - conn.error("irc.JOIN(): buh? JOIN to unknown channel %s recieved from (non-me) nick %s", line.Text, line.Nick) - return - } - ch = conn.NewChannel(chname) - // since we don't know much about this channel, ask server for info - // we get the channel users automatically in 353 and the channel - // topic in 332 on join, so we just need to get the modes - conn.Mode(ch.Name) - // sending a WHO for the channel is MUCH more efficient than - // triggering a WHOIS on every nick from the 353 handler - conn.Who(ch.Name) - } - if n == nil { - // this is the first we've seen of this nick - n = conn.NewNick(line.Nick, line.Ident, "", line.Host) - // since we don't know much about this nick, ask server for info - conn.Who(n.Nick) - } - // this takes care of both nick and channel linking \o/ - ch.AddNick(n) - }) - - // Handle PARTs from channels to maintain state - conn.AddHandler("PART", func(conn *Conn, line *Line) { - // Some IRCds (ircu) send 'PART :#chan' when there's no part message - // instead of 'PART #chan'. This is *questionable* behaviour... - var chname string - if len(line.Args) > 0 { - chname = line.Args[0] - } else if len(line.Text) > 0 { - chname = line.Text - } - ch := conn.GetChannel(chname) - n := conn.GetNick(line.Nick) - if ch != nil && n != nil { - ch.DelNick(n) - } else { - conn.error("irc.PART(): buh? PART of channel %s by nick %s", chname, line.Nick) - } - }) - - // Handle KICKs from channels to maintain state - conn.AddHandler("KICK", func(conn *Conn, line *Line) { - // XXX: this won't handle autorejoining channels on KICK - // it's trivial to do this in a seperate handler... - ch := conn.GetChannel(line.Args[0]) - n := conn.GetNick(line.Args[1]) - if ch != nil && n != nil { - ch.DelNick(n) - } else { - conn.error("irc.KICK(): buh? KICK from channel %s of nick %s", line.Args[0], line.Args[1]) - } - }) - - // Handle other people's QUITs - conn.AddHandler("QUIT", func(conn *Conn, line *Line) { - if n := conn.GetNick(line.Nick); n != nil { - n.Delete() - } else { - conn.error("irc.QUIT(): buh? QUIT from unknown nick %s", line.Nick) - } - }) - - // Handle MODE changes for channels we know about (and our nick personally) - // this is moderately ugly. suggestions for improvement welcome - conn.AddHandler("MODE", func(conn *Conn, line *Line) { - // channel modes first - if ch := conn.GetChannel(line.Args[0]); ch != nil { - modeargs := line.Args[2:len(line.Args)] - var modeop bool // true => add mode, false => remove mode - var modestr string - for i := 0; i < len(line.Args[1]); i++ { - switch m := line.Args[1][i]; m { - case '+': - modeop = true - modestr = string(m) - case '-': - modeop = false - modestr = string(m) - case 'i': - ch.Modes.InviteOnly = modeop - case 'm': - ch.Modes.Moderated = modeop - case 'n': - ch.Modes.NoExternalMsg = modeop - case 'p': - ch.Modes.Private = modeop - case 's': - ch.Modes.Secret = modeop - case 't': - ch.Modes.ProtectedTopic = modeop - case 'z': - ch.Modes.SSLOnly = modeop - case 'O': - ch.Modes.OperOnly = modeop - case 'k': - if len(modeargs) != 0 { - ch.Modes.Key, modeargs = modeargs[0], modeargs[1:len(modeargs)] - } else { - conn.error("irc.MODE(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) - } - case 'l': - if len(modeargs) != 0 { - ch.Modes.Limit, _ = strconv.Atoi(modeargs[0]) - modeargs = modeargs[1:len(modeargs)] - } else { - conn.error("irc.MODE(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) - } - case 'q', 'a', 'o', 'h', 'v': - if len(modeargs) != 0 { - n := conn.GetNick(modeargs[0]) - if p, ok := ch.Nicks[n]; ok && n != nil { - switch m { - case 'q': - p.Owner = modeop - case 'a': - p.Admin = modeop - case 'o': - p.Op = modeop - case 'h': - p.HalfOp = modeop - case 'v': - p.Voice = modeop - } - modeargs = modeargs[1:len(modeargs)] - } else { - conn.error("irc.MODE(): MODE %s %s%s %s: buh? state tracking failure.", ch.Name, modestr, m, modeargs[0]) - } - } else { - conn.error("irc.MODE(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) - } - } - } - } else if n := conn.GetNick(line.Args[0]); n != nil { - // nick mode change, should be us - if n != conn.Me { - conn.error("irc.MODE(): buh? recieved MODE %s for (non-me) nick %s", line.Text, n.Nick) - return - } - var modeop bool // true => add mode, false => remove mode - for i := 0; i < len(line.Text); i++ { - switch m := line.Text[i]; m { - case '+': - modeop = true - case '-': - modeop = false - case 'i': - n.Modes.Invisible = modeop - case 'o': - n.Modes.Oper = modeop - case 'w': - n.Modes.WallOps = modeop - case 'x': - n.Modes.HiddenHost = modeop - case 'z': - n.Modes.SSL = modeop - } - } - } else { - if line.Text != "" { - conn.error("irc.MODE(): buh? not sure what to do with nick MODE %s %s", line.Args[0], line.Text) - } else { - conn.error("irc.MODE(): buh? not sure what to do with chan MODE %s", strings.Join(line.Args, " ")) - } - } - }) - - // Handle TOPIC changes for channels - conn.AddHandler("TOPIC", func(conn *Conn, line *Line) { - if ch := conn.GetChannel(line.Args[0]); ch != nil { - ch.Topic = line.Text - } else { - conn.error("irc.TOPIC(): buh? topic change on unknown channel %s", line.Args[0]) - } - }) - - // Handle 311 whois reply - conn.AddHandler("311", func(conn *Conn, line *Line) { - if n := conn.GetNick(line.Args[1]); n != nil { - n.Ident = line.Args[2] - n.Host = line.Args[3] - n.Name = line.Text - } else { - conn.error("irc.311(): buh? received WHOIS info for unknown nick %s", line.Args[1]) - } - }) - - // Handle 324 mode reply - conn.AddHandler("324", func(conn *Conn, line *Line) { - // XXX: copypasta from MODE, needs tidying. - if ch := conn.GetChannel(line.Args[1]); ch != nil { - modeargs := line.Args[3:len(line.Args)] - var modeop bool // true => add mode, false => remove mode - var modestr string - for i := 0; i < len(line.Args[2]); i++ { - switch m := line.Args[2][i]; m { - case '+': - modeop = true - modestr = string(m) - case '-': - modeop = false - modestr = string(m) - case 'i': - ch.Modes.InviteOnly = modeop - case 'm': - ch.Modes.Moderated = modeop - case 'n': - ch.Modes.NoExternalMsg = modeop - case 'p': - ch.Modes.Private = modeop - case 's': - ch.Modes.Secret = modeop - case 't': - ch.Modes.ProtectedTopic = modeop - case 'z': - ch.Modes.SSLOnly = modeop - case 'O': - ch.Modes.OperOnly = modeop - case 'k': - if len(modeargs) != 0 { - ch.Modes.Key, modeargs = modeargs[0], modeargs[1:len(modeargs)] - } else { - conn.error("irc.324(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) - } - case 'l': - if len(modeargs) != 0 { - ch.Modes.Limit, _ = strconv.Atoi(modeargs[0]) - modeargs = modeargs[1:len(modeargs)] - } else { - conn.error("irc.324(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) - } - } - } - } else { - conn.error("irc.324(): buh? received MODE settings for unknown channel %s", line.Args[1]) - } - }) - - // Handle 332 topic reply on join to channel - conn.AddHandler("332", func(conn *Conn, line *Line) { - if ch := conn.GetChannel(line.Args[1]); ch != nil { - ch.Topic = line.Text - } else { - conn.error("irc.332(): buh? received TOPIC value for unknown channel %s", line.Args[1]) - } - }) - - // Handle 352 who reply - conn.AddHandler("352", func(conn *Conn, line *Line) { - if n := conn.GetNick(line.Args[5]); n != nil { - n.Ident = line.Args[2] - n.Host = line.Args[3] - // XXX: do we care about the actual server the nick is on? - // or the hop count to this server? - // line.Text contains " " - a := strings.Split(line.Text, " ", 2) - n.Name = a[1] - if idx := strings.Index(line.Args[6], "*"); idx != -1 { - n.Modes.Oper = true - } - if idx := strings.Index(line.Args[6], "H"); idx != -1 { - n.Modes.Invisible = true - } - } else { - conn.error("irc.352(): buh? got WHO reply for unknown nick %s", line.Args[5]) - } - }) - - // Handle 353 names reply - conn.AddHandler("353", func(conn *Conn, line *Line) { - if ch := conn.GetChannel(line.Args[2]); ch != nil { - nicks := strings.Split(line.Text, " ", -1) - for _, nick := range nicks { - // UnrealIRCd's coders are lazy and leave a trailing space - if nick == "" { - continue - } - switch c := nick[0]; c { - case '~', '&', '@', '%', '+': - nick = nick[1:len(nick)] - fallthrough - default: - n := conn.GetNick(nick) - if n == nil { - // we don't know this nick yet! - n = conn.NewNick(nick, "", "", "") - } - if n != conn.Me { - // we will be in the names list, but should also be in - // the channel's nick list from the JOIN handler above - ch.AddNick(n) - } - p := ch.Nicks[n] - switch c { - case '~': - p.Owner = true - case '&': - p.Admin = true - case '@': - p.Op = true - case '%': - p.HalfOp = true - case '+': - p.Voice = true - } - } - } - } else { - conn.error("irc.353(): buh? received NAMES list for unknown channel %s", line.Args[2]) - } - }) - - // Handle 671 whois reply (nick connected via SSL) - conn.AddHandler("671", func(conn *Conn, line *Line) { - if n := conn.GetNick(line.Args[1]); n != nil { - n.Modes.SSL = true - } else { - conn.error("irc.671(): buh? received WHOIS SSL info for unknown nick %s", line.Args[1]) - } - }) + conn.AddHandler("001", (*Conn).h_001) + conn.AddHandler("311", (*Conn).h_311) + conn.AddHandler("324", (*Conn).h_324) + conn.AddHandler("332", (*Conn).h_332) + conn.AddHandler("352", (*Conn).h_352) + conn.AddHandler("353", (*Conn).h_353) + conn.AddHandler("433", (*Conn).h_433) + conn.AddHandler("671", (*Conn).h_671) } From 00d25810b5913f6405239f0280885b7fd5703de3 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Thu, 4 Nov 2010 01:22:49 +0000 Subject: [PATCH 20/20] De-duplicate mode parsing and move to nickchan.go. --- irc/handlers.go | 135 ++---------------------------------------------- irc/nickchan.go | 92 +++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 131 deletions(-) diff --git a/irc/handlers.go b/irc/handlers.go index c1c35cb..e27be54 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -3,10 +3,7 @@ package irc // this file contains the basic set of event handlers // to manage tracking an irc connection etc. -import ( - "strings" - "strconv" -) +import "strings" // AddHandler() adds an event handler for a specific IRC command. // @@ -214,100 +211,17 @@ func (conn *Conn) h_QUIT(line *Line) { } // Handle MODE changes for channels we know about (and our nick personally) -// this is moderately ugly. suggestions for improvement welcome func (conn *Conn) h_MODE(line *Line) { // channel modes first if ch := conn.GetChannel(line.Args[0]); ch != nil { - modeargs := line.Args[2:len(line.Args)] - var modeop bool // true => add mode, false => remove mode - var modestr string - for i := 0; i < len(line.Args[1]); i++ { - switch m := line.Args[1][i]; m { - case '+': - modeop = true - modestr = string(m) - case '-': - modeop = false - modestr = string(m) - case 'i': - ch.Modes.InviteOnly = modeop - case 'm': - ch.Modes.Moderated = modeop - case 'n': - ch.Modes.NoExternalMsg = modeop - case 'p': - ch.Modes.Private = modeop - case 's': - ch.Modes.Secret = modeop - case 't': - ch.Modes.ProtectedTopic = modeop - case 'z': - ch.Modes.SSLOnly = modeop - case 'O': - ch.Modes.OperOnly = modeop - case 'k': - if len(modeargs) != 0 { - ch.Modes.Key, modeargs = modeargs[0], modeargs[1:len(modeargs)] - } else { - conn.error("irc.MODE(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) - } - case 'l': - if len(modeargs) != 0 { - ch.Modes.Limit, _ = strconv.Atoi(modeargs[0]) - modeargs = modeargs[1:len(modeargs)] - } else { - conn.error("irc.MODE(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) - } - case 'q', 'a', 'o', 'h', 'v': - if len(modeargs) != 0 { - n := conn.GetNick(modeargs[0]) - if p, ok := ch.Nicks[n]; ok && n != nil { - switch m { - case 'q': - p.Owner = modeop - case 'a': - p.Admin = modeop - case 'o': - p.Op = modeop - case 'h': - p.HalfOp = modeop - case 'v': - p.Voice = modeop - } - modeargs = modeargs[1:len(modeargs)] - } else { - conn.error("irc.MODE(): MODE %s %s%s %s: buh? state tracking failure.", ch.Name, modestr, m, modeargs[0]) - } - } else { - conn.error("irc.MODE(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) - } - } - } + conn.ParseChannelModes(ch, line.Args[1], line.Args[2:len(line.Args)]) } else if n := conn.GetNick(line.Args[0]); n != nil { // nick mode change, should be us if n != conn.Me { conn.error("irc.MODE(): buh? recieved MODE %s for (non-me) nick %s", line.Text, n.Nick) return } - var modeop bool // true => add mode, false => remove mode - for i := 0; i < len(line.Text); i++ { - switch m := line.Text[i]; m { - case '+': - modeop = true - case '-': - modeop = false - case 'i': - n.Modes.Invisible = modeop - case 'o': - n.Modes.Oper = modeop - case 'w': - n.Modes.WallOps = modeop - case 'x': - n.Modes.HiddenHost = modeop - case 'z': - n.Modes.SSL = modeop - } - } + conn.ParseNickModes(n, line.Text) } else { if line.Text != "" { conn.error("irc.MODE(): buh? not sure what to do with nick MODE %s %s", line.Args[0], line.Text) @@ -341,48 +255,7 @@ func (conn *Conn) h_311(line *Line) { func (conn *Conn) h_324(line *Line) { // XXX: copypasta from MODE, needs tidying. if ch := conn.GetChannel(line.Args[1]); ch != nil { - modeargs := line.Args[3:len(line.Args)] - var modeop bool // true => add mode, false => remove mode - var modestr string - for i := 0; i < len(line.Args[2]); i++ { - switch m := line.Args[2][i]; m { - case '+': - modeop = true - modestr = string(m) - case '-': - modeop = false - modestr = string(m) - case 'i': - ch.Modes.InviteOnly = modeop - case 'm': - ch.Modes.Moderated = modeop - case 'n': - ch.Modes.NoExternalMsg = modeop - case 'p': - ch.Modes.Private = modeop - case 's': - ch.Modes.Secret = modeop - case 't': - ch.Modes.ProtectedTopic = modeop - case 'z': - ch.Modes.SSLOnly = modeop - case 'O': - ch.Modes.OperOnly = modeop - case 'k': - if len(modeargs) != 0 { - ch.Modes.Key, modeargs = modeargs[0], modeargs[1:len(modeargs)] - } else { - conn.error("irc.324(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) - } - case 'l': - if len(modeargs) != 0 { - ch.Modes.Limit, _ = strconv.Atoi(modeargs[0]) - modeargs = modeargs[1:len(modeargs)] - } else { - conn.error("irc.324(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) - } - } - } + conn.ParseChannelModes(ch, line.Args[2], line.Args[3:len(line.Args)]) } else { conn.error("irc.324(): buh? received MODE settings for unknown channel %s", line.Args[1]) } diff --git a/irc/nickchan.go b/irc/nickchan.go index 04b182c..8323ab0 100644 --- a/irc/nickchan.go +++ b/irc/nickchan.go @@ -6,6 +6,7 @@ package irc import ( "fmt" "reflect" + "strconv" ) // A struct representing an IRC channel @@ -96,6 +97,97 @@ func (conn *Conn) GetChannel(c string) *Channel { return nil } +// Parses mode strings for a channel +func (conn *Conn) ParseChannelModes(ch *Channel, modes string, modeargs []string) { + var modeop bool // true => add mode, false => remove mode + var modestr string + for i := 0; i < len(modes); i++ { + switch m := modes[i]; m { + case '+': + modeop = true + modestr = string(m) + case '-': + modeop = false + modestr = string(m) + case 'i': + ch.Modes.InviteOnly = modeop + case 'm': + ch.Modes.Moderated = modeop + case 'n': + ch.Modes.NoExternalMsg = modeop + case 'p': + ch.Modes.Private = modeop + case 's': + ch.Modes.Secret = modeop + case 't': + ch.Modes.ProtectedTopic = modeop + case 'z': + ch.Modes.SSLOnly = modeop + case 'O': + ch.Modes.OperOnly = modeop + case 'k': + if len(modeargs) != 0 { + ch.Modes.Key, modeargs = modeargs[0], modeargs[1:len(modeargs)] + } else { + conn.error("irc.ParseChanModes(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) + } + case 'l': + if len(modeargs) != 0 { + ch.Modes.Limit, _ = strconv.Atoi(modeargs[0]) + modeargs = modeargs[1:len(modeargs)] + } else { + conn.error("irc.ParseChanModes(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) + } + case 'q', 'a', 'o', 'h', 'v': + if len(modeargs) != 0 { + n := conn.GetNick(modeargs[0]) + if p, ok := ch.Nicks[n]; ok && n != nil { + switch m { + case 'q': + p.Owner = modeop + case 'a': + p.Admin = modeop + case 'o': + p.Op = modeop + case 'h': + p.HalfOp = modeop + case 'v': + p.Voice = modeop + } + modeargs = modeargs[1:len(modeargs)] + } else { + conn.error("irc.ParseChanModes(): MODE %s %s%s %s: buh? state tracking failure.", ch.Name, modestr, m, modeargs[0]) + } + } else { + conn.error("irc.ParseChanModes(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m) + } + } + } +} + +// Parse mode strings for a nick +func (conn *Conn) ParseNickModes(n *Nick, modes string) { + var modeop bool // true => add mode, false => remove mode + for i := 0; i < len(modes); i++ { + switch m := modes[i]; m { + case '+': + modeop = true + case '-': + modeop = false + case 'i': + n.Modes.Invisible = modeop + case 'o': + n.Modes.Oper = modeop + case 'w': + n.Modes.WallOps = modeop + case 'x': + n.Modes.HiddenHost = modeop + case 'z': + n.Modes.SSL = modeop + } + } +} + /******************************************************************************\ * Channel methods for state management \******************************************************************************/