diff --git a/irc/commands.go b/irc/commands.go index 51e655b..5041bdd 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -1,9 +1,10 @@ package irc +import "strings" + // this file contains the various commands you can // send to the server using an Conn connection - // 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. @@ -27,8 +28,8 @@ 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 string) { - msg := message +func (conn *Conn) Part(channel string, message ...string) { + msg := strings.Join(message, " ") if msg != "" { msg = " :" + msg } @@ -36,8 +37,8 @@ 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 := message +func (conn *Conn) Kick(channel, nick string, message ...string) { + msg := strings.Join(message, " ") if msg != "" { msg = " :" + msg } @@ -45,8 +46,8 @@ 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 := message +func (conn *Conn) Quit(message ...string) { + msg := strings.Join(message, " ") if msg == "" { msg = "GoBye!" } @@ -67,8 +68,8 @@ 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,arg string) { - msg := arg +func (conn *Conn) Ctcp(t, ctcp string, arg ...string) { + msg := strings.Join(arg, " ") if msg != "" { msg = " " + msg } @@ -77,8 +78,8 @@ func (conn *Conn) Ctcp(t, ctcp,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 := arg +func (conn *Conn) CtcpReply(t, ctcp string, arg ...string) { + msg := strings.Join(arg, " ") if msg != "" { msg = " " + msg } @@ -86,7 +87,7 @@ func (conn *Conn) CtcpReply(t, ctcp string, arg string) { } // Version() sends a CTCP "VERSION" to the target t -func (conn *Conn) Version(t string) { conn.Ctcp(t, "VERSION","") } +func (conn *Conn) Version(t string) { conn.Ctcp(t, "VERSION") } // Action() sends a CTCP "ACTION" to the target t func (conn *Conn) Action(t, msg string) { conn.Ctcp(t, "ACTION", msg) } @@ -94,8 +95,8 @@ 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 string) { - t := topic +func (conn *Conn) Topic(channel string, topic ...string) { + t := strings.Join(topic, " ") if t != "" { t = " :" + t } @@ -109,8 +110,8 @@ func (conn *Conn) Topic(channel string, topic string) { // 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 string) { - mode := modestring +func (conn *Conn) Mode(t string, modestring ...string) { + mode := strings.Join(modestring, " ") if mode != "" { mode = " " + mode } @@ -120,10 +121,10 @@ func (conn *Conn) Mode(t string, modestring string) { // 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 string) { - msg := message +func (conn *Conn) Away(message ...string) { + msg := strings.Join(message, " ") if msg != "" { - msg = " :"+msg + msg = " :" + msg } conn.out <- "AWAY"+msg } @@ -137,4 +138,3 @@ func (conn *Conn) Invite(nick, channel string) { func (conn *Conn) Oper(user, pass string) { conn.out <- "OPER "+user+" "+pass } - diff --git a/irc/connection.go b/irc/connection.go index 0d02573..a6a0941 100644 --- a/irc/connection.go +++ b/irc/connection.go @@ -5,7 +5,6 @@ import ( "os" "net" "crypto/tls" - "crypto/rand" "fmt" "strings" "time" @@ -15,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,8 +25,9 @@ type Conn struct { out chan string connected bool - // Are we connecting via SSL? - SSL bool + // 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 @@ -75,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 @@ -87,41 +89,55 @@ 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 + } } - 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 - conn.sock = sock - conn.io = bufio.NewReadWriter( bufio.NewReader(conn.sock), bufio.NewWriter(conn.sock)) 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) @@ -136,13 +152,15 @@ func (conn *Conn) error(s string, a ...interface{}) { } // 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 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. @@ -162,7 +180,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 @@ -183,8 +201,7 @@ func (conn *Conn) recv() { conn.shutdown() break } - // chop off \r\n - s = s[0 : len(s)-2] + s = strings.Trim(s, "\r\n") if conn.Debug { fmt.Println("<- " + s) } @@ -236,7 +253,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 5166de1..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. // @@ -24,17 +21,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 @@ -85,412 +72,298 @@ func (conn *Conn) dispatchEvent(line *Line) { } } -// 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 +func (conn *Conn) h_PING(line *Line) { + conn.Raw("PONG :" + line.Text) +} - // Basic ping/pong handler - conn.AddHandler("PING", func(conn *Conn, line *Line) { conn.Raw("PONG :" + line.Text) }) - - // 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)] - } +// 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. - /* +// 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) { - ch := conn.GetChannel(line.Text) - 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(line.Text) - // 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) { - var ch *Channel - if len(line.Args) > 0 { - ch = conn.GetChannel(line.Args[0]) - } - n := conn.GetNick(line.Nick) - if ch != nil && n != nil { - ch.DelNick(n) - } else { - conn.error("irc.PART(): buh? %s", line.Raw) - } - }) - - // 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) - } - case 'b': - if len(modeargs) != 0 { - // we only care about host bans - if modeop && strings.HasPrefix(modeargs[0], "*!*@") { - for n, _ := range ch.Nicks { - if modeargs[0][4:] == n.Host { - ch.AddBan(n.Nick, modeargs[0]) - } - } - } else if !modeop { - ch.DeleteBan(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) - } - } - } - } 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]) - } - }) +// 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) +func (conn *Conn) h_MODE(line *Line) { + // channel modes first + if ch := conn.GetChannel(line.Args[0]); ch != nil { + 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 + } + 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) + } 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 { + 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]) + } +} + +// 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 +func (conn *Conn) setupEvents() { + conn.events = make(map[string][]func(*Conn, *Line)) + + 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) + + 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) } 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 27fdca2..b2db744 100644 --- a/irc/nickchan.go +++ b/irc/nickchan.go @@ -6,6 +6,8 @@ package irc import ( "fmt" "reflect" + "strconv" + "strings" ) // A struct representing an IRC channel @@ -97,6 +99,113 @@ 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) + } + case 'b': + if len(modeargs) != 0 { + // we only care about host bans + if modeop && strings.HasPrefix(modeargs[0], "*!*@") { + for n, _ := range ch.Nicks { + if modeargs[0][4:] == n.Host { + ch.AddBan(n.Nick, modeargs[0]) + } + } + } else if !modeop { + ch.DeleteBan(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) + } + } + } +} + +// 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 \******************************************************************************/ @@ -217,44 +326,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 @@ -340,7 +449,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()) } } }