diff --git a/README.md b/README.md index e5baa92..7ef8050 100644 --- a/README.md +++ b/README.md @@ -52,5 +52,11 @@ likely that this state tracking will become optional in the near future. Sorry the documentation is crap. Use the source, Luke. +[Feedback](mailto:a.bramley@gmail.com) on design decisions is welcome. I am +indebted to Matt Gruen for his work on +[go-bot](http://code.google.com/p/go-bot/source/browse/irc.go) which inspired +the re-organisation and channel-based communication structure of `*Conn.send()` +and `*Conn.recv()`. I'm sure things could be more asynchronous, still. + This code is (c) 2009 Alex Bramley, and released under the same licence terms as Go itself. diff --git a/irc/commands.go b/irc/commands.go index 708915b..2edc04e 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -4,7 +4,6 @@ package irc // send to the server using an Conn connection import ( - // "fmt"; "reflect" ) @@ -12,89 +11,100 @@ 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 } +// Raw() sends a raw line to the server, should really only be used for +// debugging purposes but may well come in handy. +func (conn *Conn) Raw(rawline string) { conn.out <- rawline } -// send a PASS command to the server -func (conn *Conn) Pass(p string) { conn.out <- "PASS "+p } +// Pass() sends a PASS command to the server +func (conn *Conn) Pass(password string) { conn.out <- "PASS "+password } -// send a NICK command to the server -func (conn *Conn) Nick(n string) { conn.out <- "NICK "+n } +// Nick() sends a NICK command to the server +func (conn *Conn) Nick(nick string) { conn.out <- "NICK "+nick } -// send a USER command to the server -func (conn *Conn) User(u, n string) { conn.out <- "USER "+u+" 12 * :"+n } +// User() sends a USER command to the server +func (conn *Conn) User(ident, name string) { + conn.out <- "USER "+ident+" 12 * :"+name +} -// send a JOIN command to the server -func (conn *Conn) Join(c string) { conn.out <- "JOIN "+c } +// Join() sends a JOIN command to the server +func (conn *Conn) Join(channel string) { conn.out <- "JOIN "+channel } -// send a PART command to the server -func (conn *Conn) Part(c string, a ...) { - msg := getStringMsg(a) +// Part() sends a PART command to the server with an optional part message +func (conn *Conn) Part(channel string, message ...) { + msg := getStringMsg(message) if msg != "" { msg = " :" + msg } - conn.out <- "PART "+c+msg + conn.out <- "PART "+channel+msg } -// send a QUIT command to the server -func (conn *Conn) Quit(a ...) { - msg := getStringMsg(a) +// Quit() sends a QUIT command to the server with an optional quit message +func (conn *Conn) Quit(message ...) { + msg := getStringMsg(message) if msg == "" { msg = "GoBye!" } conn.out <- "QUIT :"+msg } -// send a WHOIS command to the server -func (conn *Conn) Whois(t string) { conn.out <- "WHOIS "+t } +// Whois() sends a WHOIS command to the server +func (conn *Conn) Whois(nick string) { conn.out <- "WHOIS "+nick } -// send a WHO command to the server -func (conn *Conn) Who(t string) { conn.out <- "WHO "+t } +//Who() sends a WHO command to the server +func (conn *Conn) Who(nick string) { conn.out <- "WHO "+nick } -// send a PRIVMSG to the target t +// Privmsg() sends a PRIVMSG to the target t func (conn *Conn) Privmsg(t, msg string) { conn.out <- "PRIVMSG "+t+" :"+msg } -// send a NOTICE to the target t +// Notice() sends a NOTICE to the target t func (conn *Conn) Notice(t, msg string) { conn.out <- "NOTICE "+t+" :"+msg } -// send a (generic) CTCP to the target t -func (conn *Conn) Ctcp(t, ctcp string, a ...) { - msg := getStringMsg(a) +// Ctcp() sends a (generic) CTCP message to the target t +// with an optional argument +func (conn *Conn) Ctcp(t, ctcp string, arg ...) { + msg := getStringMsg(arg) if msg != "" { msg = " " + msg } conn.Privmsg(t, "\001"+ctcp+msg+"\001") } -// send a generic CTCP reply to the target t -func (conn *Conn) CtcpReply(t, ctcp string, a ...) { - msg := getStringMsg(a) +// CtcpReply() sends a generic CTCP reply to the target t +// with an optional argument +func (conn *Conn) CtcpReply(t, ctcp string, arg ...) { + msg := getStringMsg(arg) if msg != "" { msg = " " + msg } conn.Notice(t, "\001"+ctcp+msg+"\001") } -// send a CTCP "VERSION" to the target t +// Version() sends a CTCP "VERSION" to the target t func (conn *Conn) Version(t string) { conn.Ctcp(t, "VERSION") } -// send a CTCP "ACTION" to the target t -- /me does stuff! +// Action() sends a CTCP "ACTION" to the target t 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 +// 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 ...) { + t := getStringMsg(topic) + if t != "" { + t = " :" + t } - conn.out <- "TOPIC "+c+topic + conn.out <- "TOPIC "+channel+t } -// 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) +// send a MODE command to the server. This one can get complicated if we try +// to be too clever, so it's deliberately simple: +// Mode(t) retrieves the user or channel modes for target t +// Mode(t, "modestring") sets user or channel modes for target t, where... +// 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 ...) { + mode := getStringMsg(modestring) if mode != "" { mode = " " + mode } diff --git a/irc/connection.go b/irc/connection.go index 4d4c644..5bf33ec 100644 --- a/irc/connection.go +++ b/irc/connection.go @@ -8,9 +8,10 @@ import ( "strings" ) -// the IRC connection object +// An IRC connection is represented by this struct. Once connected, any errors +// encountered are piped down *Conn.Err; this channel is closed on disconnect. type Conn struct { - // Hostname, Nickname, etc. + // Connection Hostname and Nickname Host string Me *Nick @@ -34,16 +35,19 @@ type Conn struct { nicks map[string]*Nick } -// We'll parse an incoming line into this struct -// raw =~ ":nick!user@host cmd args[] :text" -// src == "nick!user@host" +// We parse an incoming line into this struct. Line.Cmd is used as the trigger +// name for incoming event handlers, see *Conn.recv() for details. +// Raw =~ ":nick!user@host cmd args[] :text" +// Src == "nick!user@host" +// Cmd == e.g. PRIVMSG, 332 type Line struct { Nick, Ident, Host, Src string Cmd, Text, Raw string Args []string } -// construct a new IRC Connection object +// Creates a new IRC connection object, but doesn't connect to anything so +// that you can add event handlers to it. See AddHandler() for details. func New(nick, user, name string) *Conn { conn := new(Conn) conn.initialise() @@ -64,8 +68,10 @@ func (conn *Conn) initialise() { conn.sock = nil } -// connect the IRC connection object to a host -func (conn *Conn) Connect(host, pass string) os.Error { +// 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 { if conn.connected { return os.NewError(fmt.Sprintf("irc.Connect(): already connected to %s, cannot connect to %s", conn.Host, host)) } @@ -87,8 +93,9 @@ func (conn *Conn) Connect(host, pass string) os.Error { go conn.send() go conn.recv() - if pass != "" { - conn.Pass(pass) + // see getStringMsg() in commands.go for what this does + if p := getStringMsg(pass); p != "" { + conn.Pass(p) } conn.Nick(conn.Me.Nick) conn.User(conn.Me.Ident, conn.Me.Name) @@ -197,6 +204,8 @@ func (conn *Conn) shutdown() { fmt.Println("irc.shutdown(): shut down sockets and channels!") } +// Dumps a load of information about the current state of the connection to a +// string for debugging state tracking and other such things. func (conn *Conn) String() string { str := "GoIRC Connection\n" str += "----------------\n\n" diff --git a/irc/handlers.go b/irc/handlers.go index 29ed521..639dd41 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -8,7 +8,19 @@ import ( "strconv" ) -// Add an event handler for a specific IRC command +// AddHandler() adds an event handler for a specific IRC command. +// +// Handlers take the form of an anonymous function (currently): +// func(conn *irc.Conn, line *irc.Line) { +// // handler code here +// } +// +// Handlers are triggered on incoming Lines from the server, with the handler +// "name" being equivalent to Line.Cmd. Read the RFCs for details on what +// replies could come from the server. They'll generally be things like +// "PRIVMSG", "JOIN", etc. but all the numeric replies are left as ascii +// strings of digits like "332" (mainly because I really didn't feel like +// putting massive constant tables in). func (conn *Conn) AddHandler(name string, f func(*Conn, *Line)) { n := strings.ToUpper(name) if e, ok := conn.events[n]; ok { diff --git a/irc/nickchan.go b/irc/nickchan.go index 7562cc9..9c6c86e 100644 --- a/irc/nickchan.go +++ b/irc/nickchan.go @@ -12,8 +12,8 @@ import ( type Channel struct { Name, Topic string Modes *ChanMode - Nicks map[*Nick]*ChanPrivs - conn *Conn + Nicks map[*Nick]*ChanPrivs + conn *Conn } // A struct representing an IRC nick @@ -25,21 +25,26 @@ type Nick struct { } // A struct representing the modes of an IRC Channel -// (the ones we care about, at least) -// see the MODE handler in setupEvents() for details +// (the ones we care about, at least). +// +// See the MODE handler in setupEvents() for details of how this is maintained. 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 { @@ -56,6 +61,9 @@ type ChanPrivs struct { /******************************************************************************\ * Conn methods to create/look up nicks/channels \******************************************************************************/ + +// Creates a new *irc.Nick, initialises it, and stores it in *irc.Conn so it +// can be properly tracked for state management purposes. func (conn *Conn) NewNick(nick, ident, name, host string) *Nick { n := &Nick{Nick: nick, Ident: ident, Name: name, Host: host, conn: conn} n.initialise() @@ -63,6 +71,7 @@ func (conn *Conn) NewNick(nick, ident, name, host string) *Nick { return n } +// Returns an *irc.Nick for the nick n, if we're tracking it. func (conn *Conn) GetNick(n string) *Nick { if nick, ok := conn.nicks[n]; ok { return nick @@ -70,6 +79,8 @@ func (conn *Conn) GetNick(n string) *Nick { return nil } +// Creates a new *irc.Channel, initialises it, and stores it in *irc.Conn so it +// can be properly tracked for state management purposes. func (conn *Conn) NewChannel(c string) *Channel { ch := &Channel{Name: c, conn: conn} ch.initialise() @@ -77,6 +88,7 @@ func (conn *Conn) NewChannel(c string) *Channel { return ch } +// Returns an *irc.Channel for the channel c, if we're tracking it. func (conn *Conn) GetChannel(c string) *Channel { if ch, ok := conn.chans[c]; ok { return ch @@ -87,11 +99,13 @@ func (conn *Conn) GetChannel(c string) *Channel { /******************************************************************************\ * Channel methods for state management \******************************************************************************/ + func (ch *Channel) initialise() { ch.Modes = new(ChanMode) ch.Nicks = make(map[*Nick]*ChanPrivs) } +// Associates an *irc.Nick with an *irc.Channel using a shared *irc.ChanPrivs func (ch *Channel) AddNick(n *Nick) { if _, ok := ch.Nicks[n]; !ok { ch.Nicks[n] = new(ChanPrivs) @@ -101,6 +115,9 @@ func (ch *Channel) AddNick(n *Nick) { } } +// Disassociates an *irc.Nick from an *irc.Channel. Will call ch.Delete() if +// the *irc.Nick being removed is the connection's nick. Will also call +// n.DelChannel(ch) to remove the association from the perspective of *irc.Nick. 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) @@ -116,6 +133,8 @@ func (ch *Channel) DelNick(n *Nick) { // consistency, and this would mean spewing an error message every delete } +// Stops the channel from being tracked by state tracking handlers. Also calls +// n.DelChannel(ch) for all nicks that are associated with the channel. func (ch *Channel) Delete() { fmt.Printf("irc.Channel.Delete(): deleting %s\n", ch.Name) for n, _ := range ch.Nicks { @@ -132,7 +151,11 @@ func (n *Nick) initialise() { n.Channels = make(map[*Channel]*ChanPrivs) } -// very slightly different to Channel.AddNick() ... +// Associates an *irc.Channel with an *irc.Nick using a shared *irc.ChanPrivs +// +// Very slightly different to irc.Channel.AddNick() in that it tests for a +// pre-existing association within the *irc.Nick object rather than the +// *irc.Channel object before associating the two. func (n *Nick) AddChannel(ch *Channel) { if _, ok := n.Channels[ch]; !ok { ch.Nicks[n] = new(ChanPrivs) @@ -142,6 +165,9 @@ func (n *Nick) AddChannel(ch *Channel) { } } +// Disassociates an *irc.Channel from an *irc.Nick. Will call n.Delete() if +// the *irc.Nick is no longer on any channels we are tracking. Will also call +// ch.DelNick(n) to remove the association from the perspective of *irc.Channel. 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) @@ -154,12 +180,16 @@ func (n *Nick) DelChannel(ch *Channel) { } } +// Signals to the tracking code that the *irc.Nick object should be tracked +// under a "neu" nick rather than the old one. func (n *Nick) ReNick(neu string) { n.conn.nicks[n.Nick] = nil, false n.Nick = neu n.conn.nicks[n.Nick] = n } +// Stops the nick from being tracked by state tracking handlers. Also calls +// ch.DelNick(n) for all nicks that are associated with the channel. func (n *Nick) Delete() { // we don't ever want to remove *our* nick from conn.nicks... if n != n.conn.Me { @@ -174,6 +204,8 @@ func (n *Nick) Delete() { /******************************************************************************\ * String() methods for all structs in this file for ease of debugging. \******************************************************************************/ + +// Map *irc.ChanMode fields to IRC mode characters var ChanModeToString = map[string]string{ "Private": "p", "Secret": "s", @@ -186,6 +218,8 @@ var ChanModeToString = map[string]string{ "Key": "k", "Limit": "l", } + +// Map *irc.NickMode fields to IRC mode characters var NickModeToString = map[string]string{ "Invisible": "i", "Oper": "o", @@ -193,6 +227,8 @@ var NickModeToString = map[string]string{ "HiddenHost": "x", "SSL": "z", } + +// Map *irc.ChanPrivs fields to IRC mode characters var ChanPrivToString = map[string]string{ "Owner": "q", "Admin": "a", @@ -200,6 +236,9 @@ var ChanPrivToString = map[string]string{ "HalfOp": "h", "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": '&', @@ -207,6 +246,8 @@ var ChanPrivToModeChar = map[string]byte{ "HalfOp": '%', "Voice": '+', } + +// Reverse mappings of the above datastructures var StringToChanMode, StringToNickMode, StringToChanPriv map[string]string var ModeCharToChanPriv map[byte]string @@ -230,6 +271,13 @@ func init() { } } +// Returns a string representing the channel. Looks like: +// Channel: e.g. #moo +// Topic: e.g. Discussing the merits of cows! +// Mode: e.g. +nsti +// Nicks: +// : e.g. CowMaster: +o +// ... func (ch *Channel) String() string { str := "Channel: " + ch.Name + "\n\t" str += "Topic: " + ch.Topic + "\n\t" @@ -241,6 +289,14 @@ func (ch *Channel) String() string { return str } +// Returns a string representing the nick. Looks like: +// Nick: e.g. CowMaster +// Hostmask: e.g. moo@cows.org +// Real Name: e.g. Steve "CowMaster" Bush +// Modes: e.g. +z +// Channels: +// : e.g. #moo: +o +// ... func (n *Nick) String() string { str := "Nick: " + n.Nick + "\n\t" str += "Hostmask: " + n.Ident + "@" + n.Host + "\n\t" @@ -253,6 +309,8 @@ func (n *Nick) String() string { return str } +// Returns a string representing the channel modes. Looks like: +// +npk key func (cm *ChanMode) String() string { str := "+" a := make([]string, 2) @@ -287,6 +345,8 @@ func (cm *ChanMode) String() string { return str } +// Returns a string representing the nick modes. Looks like: +// +iwx func (nm *NickMode) String() string { str := "+" v := reflect.Indirect(reflect.NewValue(nm)).(*reflect.StructValue) @@ -306,6 +366,8 @@ func (nm *NickMode) String() string { return str } +// Returns a string representing the channel privileges. Looks like: +// +o func (p *ChanPrivs) String() string { str := "+" v := reflect.Indirect(reflect.NewValue(p)).(*reflect.StructValue)