From 79822340b5c1533c13e8786df50960eee6617100 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Thu, 17 Dec 2009 17:22:31 +0000 Subject: [PATCH] vastly updated bot framework, now with state tracking etc. --- .gitignore | 1 + client.go | 87 ++++++++++-- irc/Makefile | 1 + irc/commands.go | 104 +++++++++----- irc/connection.go | 285 +++++++++++++++++++++++--------------- irc/handlers.go | 346 +++++++++++++++++++++++++++++++++++++++++++--- irc/nickchan.go | 327 +++++++++++++++++++++++++++++++++++++++++++ vims | 1 + 8 files changed, 977 insertions(+), 175 deletions(-) create mode 100644 irc/nickchan.go create mode 100644 vims diff --git a/.gitignore b/.gitignore index f75fbc3..625c3f0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ _obj/ *.swp *~ +*.out diff --git a/client.go b/client.go index ad5865d..9b225b6 100644 --- a/client.go +++ b/client.go @@ -4,32 +4,89 @@ import ( "./irc/_obj/irc"; "fmt"; "os"; + "bufio"; + "strings"; ) func main() { + // create new IRC connection c := irc.New("GoTest", "gotest", "GoBot"); c.AddHandler("connected", - func(conn *irc.IRCConn, line *irc.IRCLine) { + func(conn *irc.Conn, line *irc.Line) { conn.Join("#"); } ); - c.AddHandler("join", - func(conn *irc.IRCConn, line *irc.IRCLine) { - if line.Nick == conn.Me { - conn.Privmsg(line.Text, "I LIVE, BITCHES"); - } - } - ); + + // connect to server if err := c.Connect("irc.pl0rt.org", ""); err != nil { - fmt.Printf("Connection error: %v\n", err); + fmt.Printf("Connection error: %s\n", err); return; } - // if we get here, we're successfully connected and should have just - // dispatched the "CONNECTED" event to it's handlers \o/ - control := make(chan os.Error, 1); - go c.RunLoop(control); - if err := <-control; err != nil { - fmt.Printf("IRCConn.RunLoop terminated: %v\n", err); + // set up a goroutine to read commands from stdin + in := make(chan string, 4); + reallyquit := false; + go func() { + con := bufio.NewReader(os.Stdin); + for { + s, err := con.ReadString('\n'); + if err != nil { + // wha?, maybe ctrl-D... + close(in); + break; + } + // no point in sending empty lines down the channel + if len(s) > 2 { + in <- s[0:len(s)-1] + } + } + }(); + + // set up a goroutine to do parsey things with the stuff from stdin + go func() { + for { + if closed(in) { + break; + } + cmd := <-in; + if cmd[0] == ':' { + switch idx := strings.Index(cmd, " "); { + case idx == -1: + continue; + case cmd[1] == 'q': + reallyquit = true; + c.Quit(cmd[idx+1:len(cmd)]); + case cmd[1] == 'j': + c.Join(cmd[idx+1:len(cmd)]); + case cmd[1] == 'p': + c.Part(cmd[idx+1:len(cmd)]); + case cmd[1] == 'd': + fmt.Printf(c.String()); + } + } else { + c.Raw(cmd) + } + } + }(); + + // stall here waiting for asplode on error channel + for { + if closed(c.Err) { + // c.Err being closed indicates we've been disconnected from the + // server for some reason (e.g. quit, kill or ping timeout) + // if we don't really want to quit, reconnect! + if !reallyquit { + fmt.Println("Reconnecting..."); + if err := c.Connect("irc.pl0rt.org", ""); err != nil { + fmt.Printf("Connection error: %s\n", err); + break; + } + continue; + } + break; + } + if err := <-c.Err; err != nil { + fmt.Printf("goirc error: %s\n", err); + } } } diff --git a/irc/Makefile b/irc/Makefile index 07ef058..08e7382 100644 --- a/irc/Makefile +++ b/irc/Makefile @@ -9,5 +9,6 @@ GOFILES=\ connection.go\ commands.go\ handlers.go\ + nickchan.go include $(GOROOT)/src/Make.pkg diff --git a/irc/commands.go b/irc/commands.go index b5d21e5..f06fd8d 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -1,10 +1,10 @@ package irc // this file contains the various commands you can -// send to the server using an IRCConn connection +// send to the server using an Conn connection import ( - "fmt"; +// "fmt"; "reflect"; ) @@ -12,91 +12,127 @@ import ( // the symbol table and add methods/functions on the fly // [ CMD, FMT, FMTARGS ] etc. +// send a raw line to the server for debugging etc +func (conn *Conn) Raw(s string) { + conn.out <- s +} + // send a PASS command to the server -func (conn *IRCConn) Pass(p string) { - conn.send(fmt.Sprintf("PASS %s", p)); +func (conn *Conn) Pass(p string) { + conn.out <- "PASS " + p } // send a NICK command to the server -func (conn *IRCConn) Nick(n string) { - conn.send(fmt.Sprintf("NICK %s", n)); +func (conn *Conn) Nick(n string) { + conn.out <- "NICK " + n } // send a USER command to the server -func (conn *IRCConn) User(u, n string) { - conn.send(fmt.Sprintf("USER %s 12 * :%s", u, n)); +func (conn *Conn) User(u, n string) { + conn.out <- "USER " + u + " 12 * :" + n } // send a JOIN command to the server -func (conn *IRCConn) Join(c string) { - conn.send(fmt.Sprintf("JOIN %s", c)); +func (conn *Conn) Join(c string) { + conn.out <- "JOIN " + c } // send a PART command to the server -func (conn *IRCConn) Part(c string, a ...) { +func (conn *Conn) Part(c string, a ...) { msg := getStringMsg(a); if msg != "" { msg = " :" + msg } - conn.send(fmt.Sprintf("PART %s%s", c, msg)); + conn.out <- "PART " + c + msg } // send a QUIT command to the server -func (conn *IRCConn) Quit(a ...) { +func (conn *Conn) Quit(a ...) { msg := getStringMsg(a); if msg == "" { msg = "GoBye!" } - conn.send(fmt.Sprintf("QUIT :%s", msg)); + conn.out <- "QUIT :" + msg +} + +// send a WHOIS command to the server +func (conn *Conn) Whois(t string) { + conn.out <- "WHOIS " + t +} + +// send a WHO command to the server +func (conn *Conn) Who(t string) { + conn.out <- "WHO " + t } // send a PRIVMSG to the target t -func (conn *IRCConn) Privmsg(t, msg string) { - conn.send(fmt.Sprintf("PRIVMSG %s :%s", t, msg)); +func (conn *Conn) Privmsg(t, msg string) { + conn.out <- "PRIVMSG " + t + " :" + msg } // send a NOTICE to the target t -func (conn *IRCConn) Notice(t, msg string) { - conn.send(fmt.Sprintf("NOTICE %s :%s", t, msg)); +func (conn *Conn) Notice(t, msg string) { + conn.out <- "NOTICE " + t + " :" +msg } // send a (generic) CTCP to the target t -func (conn *IRCConn) Ctcp(t, ctcp string, a ...) { +func (conn *Conn) Ctcp(t, ctcp string, a ...) { msg := getStringMsg(a); if msg != "" { msg = " " + msg } - conn.Privmsg(t, fmt.Sprintf("\001%s%s\001", ctcp, msg)); + conn.Privmsg(t, "\001" + ctcp + msg + "\001") } // send a generic CTCP reply to the target t -func (conn *IRCConn) CtcpReply(t, ctcp string, a ...) { +func (conn *Conn) CtcpReply(t, ctcp string, a ...) { msg := getStringMsg(a); if msg != "" { msg = " " + msg } - conn.Notice(t, fmt.Sprintf("\001%s%s\001", ctcp, msg)); + conn.Notice(t, "\001" + ctcp + msg + "\001") } - // send a CTCP "VERSION" to the target t -func (conn *IRCConn) Version(t string) { - conn.Ctcp(t, "VERSION"); +func (conn *Conn) Version(t string) { + conn.Ctcp(t, "VERSION") } // send a CTCP "ACTION" to the target t -- /me does stuff! -func (conn *IRCConn) Action(t, msg string) { - conn.Ctcp(t, "ACTION", msg); +func (conn *Conn) Action(t, msg string) { + conn.Ctcp(t, "ACTION", msg) +} + +// send a TOPIC command to the channel c +func (conn *Conn) Topic(c string, a ...) { + topic := getStringMsg(a); + if topic != "" { + topic = " :" + topic + } + conn.out <- "TOPIC " + c + topic +} + +// send a MODE command (this one gets complicated) +// Mode(t) retrieves the user or channel modes for target t +// Mode(t, "string" +func (conn *Conn) Mode(t string, a ...) { + mode := getStringMsg(a); + if mode != "" { + mode = " " + mode + } + conn.out <- "MODE " + t + mode } func getStringMsg(a ...) (msg string) { // dealing with functions with a variable parameter list is nasteeh :-( - // the below stolen and munged from fmt/print.go - if v := reflect.NewValue(a).(*reflect.StructValue); v.NumField() == 1 { - // XXX: should we check that this looks at least vaguely stringy first? - msg = fmt.Sprintf("%v", v.Field(1)); - } else { - msg = "" + // 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) + } } - return + return "" } diff --git a/irc/connection.go b/irc/connection.go index c3b97be..667bea6 100644 --- a/irc/connection.go +++ b/irc/connection.go @@ -1,5 +1,3 @@ -// Some IRC testing code! - package irc import ( @@ -11,96 +9,99 @@ import ( ) // the IRC connection object -type IRCConn struct { - sock *bufio.ReadWriter; - Host string; - Me string; - Ident string; - Name string; - con bool; - reg bool; - events map[string] []func (*IRCConn, *IRCLine); - chans map[string] *IRCChan; - nicks map[string] *IRCNick; +type Conn struct { + // Hostname, Nickname, etc. + Host string; + Me *Nick; + + // I/O stuff to server + sock *net.TCPConn; + io *bufio.ReadWriter; + in chan *Line; + out chan string; + connected bool; + + // Error channel to transmit any fail back to the user + Err chan os.Error; + + // Event handler mapping + events map[string] []func (*Conn, *Line); + + // Map of channels we're on + chans map[string] *Channel; + + // Map of nicks we know about + nicks map[string] *Nick; } // We'll parse an incoming line into this struct // raw =~ ":nick!user@host cmd args[] :text" // src == "nick!user@host" -type IRCLine struct { - Nick string; - User string; - Host string; - Src string; - Cmd string; +type Line struct { + Nick, Ident, Host, Src string; + Cmd, Text, Raw string; Args []string; - Text string; - Raw string; -} - -// A struct representing an IRC channel -type IRCChan struct { - Name string; - Topic string; - Modes map[string] string; - Nicks map[string] *IRCNick; -} - -// A struct representing an IRC nick -type IRCNick struct { - Name string; - Chans map[string] *IRCChan; } // construct a new IRC Connection object -func New(nick, user, name string) (conn *IRCConn) { - conn = &IRCConn{Me: nick, Ident: user, Name: name}; - // allocate meh some memoraaaahh - conn.nicks = make(map[string] *IRCNick); - conn.chans = make(map[string] *IRCChan); - conn.events = make(map[string] []func(*IRCConn, *IRCLine)); +func New(nick, user, name string) *Conn { + conn := new(Conn); + conn.initialise(); + conn.Me = conn.NewNick(nick, user, name, ""); conn.setupEvents(); - return conn + return conn; +} + +func (conn *Conn) initialise() { + // allocate meh some memoraaaahh + fmt.Println("irc.initialise(): initialising..."); + conn.nicks = make(map[string] *Nick); + conn.chans = make(map[string] *Channel); + conn.in = make(chan *Line, 32); + conn.out = make(chan string, 32); + conn.Err = make(chan os.Error, 4); + conn.io = nil; + conn.sock = nil; } // connect the IRC connection object to a host -func (conn *IRCConn) Connect(host, pass string) (err os.Error) { +func (conn *Conn) Connect(host, 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"; } - sock, err := net.Dial("tcp", "", host); - if err != nil { + + if addr, err := net.ResolveTCPAddr(host); err != nil { + return err + } else if conn.sock, err = net.DialTCP("tcp", nil, addr); err != nil { return err } - conn.sock = bufio.NewReadWriter(bufio.NewReader(sock), bufio.NewWriter(sock)); - conn.con = true; + fmt.Println("irc.Connect(): connected happily..."); conn.Host = host; - // initial connection set-up - // verify valid nick/user/name here? + conn.io = bufio.NewReadWriter( + bufio.NewReader(conn.sock), + bufio.NewWriter(conn.sock) + ); + go conn.send(); + go conn.recv(); + if pass != "" { conn.Pass(pass) } - conn.Nick(conn.Me); - conn.User(conn.Ident, conn.Name); + conn.Nick(conn.Me.Nick); + conn.User(conn.Me.Ident, conn.Me.Name); - for line, err := conn.recv(); err == nil; line, err = conn.recv() { - // initial loop to get us to the point where we're connected - conn.dispatchEvent(line); - if line.Cmd == "001" { - break; - } - } - return err; + go conn.runLoop(); + fmt.Println("irc.Connect(): launched runLoop() goroutine."); + return nil; } -func (conn *IRCConn) RunLoop(c chan os.Error) { - var err os.Error; - for line, err := conn.recv(); err == nil; line, err = conn.recv() { - conn.dispatchEvent(line); - } - c <- err; - return; +// 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)); } // copied from http.client for great justice @@ -108,56 +109,120 @@ func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") } -// send \r\n terminated line to peer, propagate errors -func (conn *IRCConn) send(line string) (err os.Error) { - err = conn.sock.WriteString(line + "\r\n"); - conn.sock.Flush(); - fmt.Println("-> " + line); - return err +// dispatch input from channel as \r\n terminated line to peer +func (conn *Conn) send() { + for { + line := <-conn.out; + if closed(conn.out) { + break; + } + if err := conn.io.WriteString(line + "\r\n"); err != nil { + conn.error("irc.send(): %s", err.String()); + conn.shutdown(); + break; + } + conn.io.Flush(); + fmt.Println("-> " + line); + } } -// receive one \r\n terminated line from peer and parse it, propagate errors -func (conn *IRCConn) recv() (line *IRCLine, err os.Error) { - s, err := conn.sock.ReadString('\n'); - if err != nil { - return line, err - } - // chop off \r\n - s = s[0:len(s)-2]; - fmt.Println("<- " + s); +// receive one \r\n terminated line from peer, parse and dispatch it +func (conn *Conn) recv() { + for { + s, err := conn.io.ReadString('\n'); + if err != nil { + conn.error("irc.recv(): %s", err.String()); + conn.shutdown(); + break; + } + // chop off \r\n + s = s[0:len(s)-2]; + fmt.Println("<- " + s); - line = &IRCLine{Raw: s}; - if s[0] == ':' { - // remove a source and parse it - if idx := strings.Index(s, " "); idx != -1 { - line.Src, s = s[1:idx], s[idx+1:len(s)]; - } else { - // pretty sure we shouldn't get here ... - line.Src = s[1:len(s)]; - return line, nil; + line := &Line{Raw: s}; + if s[0] == ':' { + // remove a source and parse it + if idx := strings.Index(s, " "); idx != -1 { + line.Src, s = s[1:idx], s[idx+1:len(s)]; + } else { + // pretty sure we shouldn't get here ... + line.Src = s[1:len(s)]; + conn.in <- line; + continue; + } + + // src can be the hostname of the irc server or a nick!user@host + line.Host = line.Src; + nidx, uidx := strings.Index(line.Src, "!"), strings.Index(line.Src, "@"); + if uidx != -1 && nidx != -1 { + line.Nick = line.Src[0:nidx]; + line.Ident = line.Src[nidx+1:uidx]; + line.Host = line.Src[uidx+1:len(line.Src)]; + } } - // src can be the hostname of the irc server or a nick!user@host - line.Host = line.Src; - nidx, uidx := strings.Index(line.Src, "!"), strings.Index(line.Src, "@"); - if uidx != -1 && nidx != -1 { - line.Nick = line.Src[0:nidx]; - line.User = line.Src[nidx+1:uidx]; - line.Host = line.Src[uidx+1:len(line.Src)]; + // now we're here, we've parsed a :nick!user@host or :server off + // s should contain "cmd args[] :text" + args := strings.Split(s, " :", 2); + if len(args) > 1 { + line.Text = args[1]; } + args = strings.Split(args[0], " ", 0); + line.Cmd = strings.ToUpper(args[0]); + if len(args) > 1 { + line.Args = args[1:len(args)]; + } + conn.in <- line } - - // now we're here, we've parsed a :nick!user@host or :server off - // s should contain "cmd args[] :text" - args := strings.Split(s, " :", 2); - if len(args) > 1 { - line.Text = args[1]; - } - args = strings.Split(args[0], " ", 0); - line.Cmd = strings.ToUpper(args[0]); - if len(args) > 1 { - line.Args = args[1:len(args)]; - } - return line, nil +} + +func (conn *Conn) runLoop() { + for { + if closed(conn.in) { + break; + } + select { + case line := <-conn.in: + conn.dispatchEvent(line); + } + } + fmt.Println("irc.runLoop(): Exited runloop..."); + // if we fall off the end here due to shutdown, + // reinit everything once the runloop is done + // so that Connect() can be called again. + conn.initialise(); +} + +func (conn *Conn) shutdown() { + close(conn.in); + close(conn.out); + close(conn.Err); + conn.connected = false; + conn.sock.Close(); + fmt.Println("irc.shutdown(): shut down sockets and channels!"); +} + +func (conn *Conn) String() string { + str := "GoIRC Connection\n"; + str += "----------------\n\n"; + if conn.connected { + str += "Connected to " + conn.Host + "\n\n" + } else { + str += "Not currently connected!\n\n"; + } + str += conn.Me.String() + "\n"; + str += "GoIRC Channels\n"; + str += "--------------\n\n"; + for _, ch := range conn.chans { + str += ch.String() + "\n" + } + str += "GoIRC NickNames\n"; + str += "---------------\n\n"; + for _, n := range conn.nicks { + if n != conn.Me { + str += n.String() + "\n" + } + } + return str; } diff --git a/irc/handlers.go b/irc/handlers.go index 700cf14..8225c07 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -4,17 +4,17 @@ package irc // to manage tracking an irc connection etc. import ( - "fmt"; "strings"; + "strconv"; ) // Add an event handler for a specific IRC command -func (conn *IRCConn) AddHandler(name string, f func (*IRCConn, *IRCLine)) { +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 (*IRCConn, *IRCLine), len(e), len(e)+10); + ne := make([]func (*Conn, *Line), len(e), len(e)+10); for i := 0; i 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.Args[1], 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 { + conn.error("irc.MODE(): buh? not sure what to do with MODE %s %s", line.Args[0], line.Args[1]); + } + }); + + // 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 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); + 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, "", "", ""); + conn.Whois(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]); + } }); } - diff --git a/irc/nickchan.go b/irc/nickchan.go new file mode 100644 index 0000000..230b9f5 --- /dev/null +++ b/irc/nickchan.go @@ -0,0 +1,327 @@ +package irc + +// Here you'll find the Channel and Nick structs +// as well as the internal state maintenance code for the handlers + +import ( + "fmt"; + "reflect"; +) + +// A struct representing an IRC channel +type Channel struct { + Name, Topic string; + Modes *ChanMode; + // MODE +q, +a, +o, +h, +v + Nicks map[*Nick] *ChanPrivs; + conn *Conn; +} + +// A struct representing an IRC nick +type Nick struct { + Nick, Ident, Host, Name string; + Modes *NickMode; + Channels map[*Channel] *ChanPrivs; + conn *Conn; +} + +// A struct representing the modes of an IRC Channel +// (the ones we care about, at least) +// see the MODE handler in setupEvents() for details +type ChanMode struct { + // MODE +p, +s, +t, +n, +m + Private, Secret, ProtectedTopic, NoExternalMsg, Moderated bool; + // MODE +i, +O, +z + InviteOnly, OperOnly, SSLOnly bool; + // MODE +k + Key string; + // MODE +l + Limit int; +} + +// A struct representing the modes of an IRC Nick (User Modes) +// (again, only the ones we care about) +// This is only really useful for conn.Me, as we can't see other people's modes +// without IRC operator privileges (and even then only on some IRCd's). +type NickMode struct { + // MODE +i, +o, +w, +x, +z + Invisible, Oper, WallOps, HiddenHost, SSL bool; +} + +// A struct representing the modes a Nick can have on a Channel +type ChanPrivs struct { + // MODE +q, +a, +o, +h, +v + Owner, Admin, Op, HalfOp, Voice bool; +} + +/******************************************************************************\ + * Conn methods to create/look up nicks/channels +\******************************************************************************/ +func (conn *Conn) NewNick(nick, ident, name, host string) *Nick { + n := &Nick{Nick: nick, Ident: ident, Name: name, Host: host, conn: conn}; + n.initialise(); + conn.nicks[n.Nick] = n; + return n; +} + +func (conn *Conn) GetNick(n string) *Nick { + if nick, ok := conn.nicks[n]; ok { + return nick + } + return nil +} + +func (conn *Conn) NewChannel(c string) *Channel { + ch := &Channel{Name: c, conn: conn}; + ch.initialise(); + conn.chans[ch.Name] = ch; + return ch +} + +func (conn *Conn) GetChannel(c string) *Channel { + if ch, ok := conn.chans[c]; ok { + return ch + } + return nil +} + +/******************************************************************************\ + * Channel methods for state management +\******************************************************************************/ +func (ch *Channel) initialise() { + ch.Modes = new(ChanMode); + ch.Nicks = make(map[*Nick] *ChanPrivs); +} + +func (ch *Channel) AddNick(n *Nick) { + if _, ok := ch.Nicks[n]; !ok { + ch.Nicks[n] = new(ChanPrivs); + n.Channels[ch] = ch.Nicks[n]; + } else { + ch.conn.error("irc.Channel.AddNick() warning: trying to add already-present nick %s to channel %s", n.Nick, ch.Name); + } +} + +func (ch *Channel) DelNick(n *Nick) { + if _, ok := ch.Nicks[n]; ok { + fmt.Printf("irc.Channel.DelNick(): deleting %s from %s\n", n.Nick, ch.Name); + if n == n.conn.Me { + // we're leaving the channel, so remove all state we have about it + ch.Delete(); + } else { + ch.Nicks[n] = nil, false; + n.DelChannel(ch); + } + } // no else here ... + // we call Channel.DelNick() and Nick.DelChan() from each other to ensure + // consistency, and this would mean spewing an error message every delete +} + +func (ch *Channel) Delete() { + fmt.Printf("irc.Channel.Delete(): deleting %s\n", ch.Name); + for n, _ := range ch.Nicks { + n.DelChannel(ch); + } + ch.conn.chans[ch.Name] = nil, false; +} + +/******************************************************************************\ + * Nick methods for state management +\******************************************************************************/ +func (n *Nick) initialise() { + n.Modes = new(NickMode); + n.Channels = make(map[*Channel] *ChanPrivs); +} + +// very slightly different to Channel.AddNick() ... +func (n *Nick) AddChannel(ch *Channel) { + if _, ok := n.Channels[ch]; !ok { + ch.Nicks[n] = new(ChanPrivs); + n.Channels[ch] = ch.Nicks[n]; + } else { + n.conn.error("irc.Nick.AddChannel() warning: trying to add already-present channel %s to nick %s", ch.Name, n.Nick); + } +} + +func (n *Nick) DelChannel(ch *Channel) { + if _, ok := n.Channels[ch]; ok { + fmt.Printf("irc.Nick.DelChannel(): deleting %s from %s\n", n.Nick, ch.Name); + n.Channels[ch] = nil, false; + ch.DelNick(n); + if len(n.Channels) == 0 { + // nick is no longer in any channels we inhabit, stop tracking it + n.Delete(); + } + } +} + +func (n *Nick) ReNick(neu string) { + n.conn.nicks[n.Nick] = nil, false; + n.Nick = neu; + n.conn.nicks[n.Nick] = n; +} + +func (n *Nick) Delete() { + // we don't ever want to remove *our* nick from conn.nicks... + if n != n.conn.Me { + fmt.Printf("irc.Nick.Delete(): deleting %s\n", n.Nick); + for ch, _ := range n.Channels { + ch.DelNick(n); + } + n.conn.nicks[n.Nick] = nil, false; + } +} + +/******************************************************************************\ + * String() methods for all structs in this file for ease of debugging. +\******************************************************************************/ +var ChanModeToString = map[string]string{ + "Private":"p", + "Secret":"s", + "ProtectedTopic":"t", + "NoExternalMsg":"n", + "Moderated":"m", + "InviteOnly":"i", + "OperOnly":"O", + "SSLOnly":"z", + "Key":"k", + "Limit":"l", +}; +var NickModeToString = map[string]string{ + "Invisible":"i", + "Oper":"o", + "WallOps":"w", + "HiddenHost":"x", + "SSL":"z", +}; +var ChanPrivToString = map[string]string{ + "Owner":"q", + "Admin":"a", + "Op":"o", + "HalfOp":"h", + "Voice":"v", +}; +var ChanPrivToModeChar = map[string]byte{ + "Owner":'~', + "Admin":'&', + "Op":'@', + "HalfOp":'%', + "Voice":'+', +}; +var StringToChanMode, StringToNickMode, StringToChanPriv map[string]string; +var ModeCharToChanPriv map[byte]string; + +// Init function to fill in reverse mappings for *toString constants etc. +func init() { + StringToChanMode = make(map[string]string); + for k,v := range ChanModeToString { + StringToChanMode[v] = k; + } + StringToNickMode = make(map[string]string); + for k,v := range NickModeToString { + StringToNickMode[v] = k; + } + StringToChanPriv = make(map[string]string); + for k,v := range ChanPrivToString { + StringToChanPriv[v] = k; + } + ModeCharToChanPriv = make(map[byte]string); + for k,v := range ChanPrivToModeChar { + ModeCharToChanPriv[v] = k; + } +} + +func (ch *Channel) String() string { + str := "Channel: " + ch.Name + "\n\t"; + str += "Topic: " + ch.Topic + "\n\t"; + str += "Modes: " + ch.Modes.String() + "\n\t"; + str += "Nicks: \n"; + for n, p := range ch.Nicks { + str += "\t\t" + n.Nick + ": " + p.String() + "\n"; + } + return str; +} + +func (n *Nick) String() string { + str := "Nick: "+ n.Nick + "\n\t"; + str += "Hostmask: " + n.Ident + "@" + n.Host + "\n\t"; + str += "Real Name: " + n.Name + "\n\t"; + str += "Modes: " + n.Modes.String() + "\n\t"; + str += "Channels: \n"; + for ch, p := range n.Channels { + str += "\t\t" + ch.Name + ": " + p.String() + "\n"; + } + return str; +} + +func (cm *ChanMode) String() string { + str := "+"; + a := make([]string, 2); + v := reflect.Indirect(reflect.NewValue(cm)).(*reflect.StructValue); + t := v.Type().(*reflect.StructType); + for i := 0; i < v.NumField(); i++ { + switch f := v.Field(i).(type) { + case *reflect.BoolValue: + if f.Get() { + str += ChanModeToString[t.Field(i).Name]; + } + case *reflect.StringValue: + if f.Get() != "" { + str += ChanModeToString[t.Field(i).Name]; + a[0] = f.Get(); + } + case *reflect.IntValue: + if f.Get() != 0 { + str += ChanModeToString[t.Field(i).Name]; + a[1] = fmt.Sprintf("%d", cm.Limit); + } + } + } + for _, s := range a { + if s != "" { + str += " " + s; + } + } + if str == "+" { + str = "No modes set"; + } + return str; +} + +func (nm *NickMode) String() string { + str := "+"; + v := reflect.Indirect(reflect.NewValue(nm)).(*reflect.StructValue); + t := v.Type().(*reflect.StructType); + for i := 0; i < v.NumField(); i++ { + switch f := v.Field(i).(type) { + // only bools here at the mo! + case *reflect.BoolValue: + if f.Get() { + str += NickModeToString[t.Field(i).Name]; + } + } + } + if str == "+" { + str = "No modes set"; + } + return str; +} + +func (p *ChanPrivs) String() string { + str := "+"; + v := reflect.Indirect(reflect.NewValue(p)).(*reflect.StructValue); + t := v.Type().(*reflect.StructType); + for i := 0; i < v.NumField(); i++ { + switch f := v.Field(i).(type) { + // only bools here at the mo too! + case *reflect.BoolValue: + if f.Get() { + str += ChanPrivToString[t.Field(i).Name]; + } + } + } + if str == "+" { + str = "No modes set"; + } + return str; +} diff --git a/vims b/vims new file mode 100644 index 0000000..8f5f265 --- /dev/null +++ b/vims @@ -0,0 +1 @@ +find . -name \*.go | xargs gvim -p