From ed92e36e8ee1407cc2d33d65836a789bfaf3e522 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Wed, 15 Apr 2015 22:27:50 +0100 Subject: [PATCH] Attempt to improve the godoc of client. --- client/commands.go | 106 ++++++++++++++++++++++------------- client/connection.go | 128 +++++++++++++++++++++++++++++++++---------- client/dispatch.go | 45 ++++++++++----- client/doc.go | 34 ++++++++++++ client/line.go | 30 +++++++--- 5 files changed, 251 insertions(+), 92 deletions(-) create mode 100644 client/doc.go diff --git a/client/commands.go b/client/commands.go index 164e04e..95b79e9 100644 --- a/client/commands.go +++ b/client/commands.go @@ -29,9 +29,10 @@ const ( VHOST = "VHOST" WHO = "WHO" WHOIS = "WHOIS" + defaultSplit = 450 ) -// cutNewLines() pares down a string to the part before the first "\r" or "\n" +// cutNewLines() pares down a string to the part before the first "\r" or "\n". func cutNewLines(s string) string { r := strings.SplitN(s, "\r", 2) r = strings.SplitN(r[0], "\n", 2) @@ -65,7 +66,7 @@ func indexFragment(s string) int { func splitMessage(msg string, splitLen int) (msgs []string) { // This is quite short ;-) if splitLen < 10 { - splitLen = 10 + splitLen = defaultSplit } for len(msg) > splitLen { idx := indexFragment(msg[:splitLen]) @@ -78,25 +79,29 @@ func splitMessage(msg string, splitLen int) (msgs []string) { return append(msgs, msg) } -// Raw() sends a raw line to the server, should really only be used for +// 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) { // Avoid command injection by enforcing one command per line. conn.out <- cutNewLines(rawline) } -// Pass() sends a PASS command to the server +// Pass sends a PASS command to the server. +// PASS password func (conn *Conn) Pass(password string) { conn.Raw(PASS + " " + password) } -// Nick() sends a NICK command to the server +// Nick sends a NICK command to the server. +// NICK nick func (conn *Conn) Nick(nick string) { conn.Raw(NICK + " " + nick) } -// User() sends a USER command to the server +// User sends a USER command to the server. +// USER ident 12 * :name func (conn *Conn) User(ident, name string) { conn.Raw(USER + " " + ident + " 12 * :" + name) } -// Join() sends a JOIN command to the server with an optional key +// Join sends a JOIN command to the server with an optional key. +// JOIN channel [key] func (conn *Conn) Join(channel string, key ...string) { k := "" if len(key) > 0 { @@ -105,7 +110,8 @@ func (conn *Conn) Join(channel string, key ...string) { conn.Raw(JOIN + " " + channel + k) } -// Part() sends a PART command to the server with an optional part message +// Part sends a PART command to the server with an optional part message. +// PART channel [:message] func (conn *Conn) Part(channel string, message ...string) { msg := strings.Join(message, " ") if msg != "" { @@ -114,7 +120,8 @@ func (conn *Conn) Part(channel string, message ...string) { conn.Raw(PART + " " + channel + msg) } -// Kick() sends a KICK command to remove a nick from a channel +// Kick sends a KICK command to remove a nick from a channel. +// KICK channel nick [:message] func (conn *Conn) Kick(channel, nick string, message ...string) { msg := strings.Join(message, " ") if msg != "" { @@ -123,7 +130,8 @@ func (conn *Conn) Kick(channel, nick string, message ...string) { conn.Raw(KICK + " " + channel + " " + nick + msg) } -// Quit() sends a QUIT command to the server with an optional quit message +// Quit sends a QUIT command to the server with an optional quit message. +// QUIT [:message] func (conn *Conn) Quit(message ...string) { msg := strings.Join(message, " ") if msg == "" { @@ -132,28 +140,37 @@ func (conn *Conn) Quit(message ...string) { conn.Raw(QUIT + " :" + msg) } -// Whois() sends a WHOIS command to the server +// Whois sends a WHOIS command to the server. +// WHOIS nick func (conn *Conn) Whois(nick string) { conn.Raw(WHOIS + " " + nick) } -//Who() sends a WHO command to the server +// Who sends a WHO command to the server. +// WHO nick func (conn *Conn) Who(nick string) { conn.Raw(WHO + " " + nick) } -// Privmsg() sends a PRIVMSG to the target t +// Privmsg sends a PRIVMSG to the target nick or channel t. +// If msg is longer than Config.SplitLen characters, multiple PRIVMSGs +// will be sent to the target containing sequential parts of msg. +// PRIVMSG t :msg func (conn *Conn) Privmsg(t, msg string) { for _, s := range splitMessage(msg, conn.cfg.SplitLen) { conn.Raw(PRIVMSG + " " + t + " :" + s) } } -// Notice() sends a NOTICE to the target t +// Notice sends a NOTICE to the target nick or channel t. +// If msg is longer than Config.SplitLen characters, multiple NOTICEs +// will be sent to the target containing sequential parts of msg. +// NOTICE t :msg func (conn *Conn) Notice(t, msg string) { for _, s := range splitMessage(msg, conn.cfg.SplitLen) { conn.Raw(NOTICE + " " + t + " :" + s) } } -// Ctcp() sends a (generic) CTCP message to the target t -// with an optional argument +// Ctcp sends a (generic) CTCP message to the target nick +// or channel t, with an optional argument. +// PRIVMSG t :\001CTCP arg\001 func (conn *Conn) Ctcp(t, ctcp string, arg ...string) { // We need to split again here to ensure for _, s := range splitMessage(strings.Join(arg, " "), conn.cfg.SplitLen) { @@ -165,8 +182,9 @@ func (conn *Conn) Ctcp(t, ctcp string, arg ...string) { } } -// CtcpReply() sends a generic CTCP reply to the target t -// with an optional argument +// CtcpReply sends a (generic) CTCP reply to the target nick +// or channel t, with an optional argument. +// NOTICE t :\001CTCP arg\001 func (conn *Conn) CtcpReply(t, ctcp string, arg ...string) { for _, s := range splitMessage(strings.Join(arg, " "), conn.cfg.SplitLen) { if s != "" { @@ -177,15 +195,18 @@ func (conn *Conn) CtcpReply(t, ctcp string, arg ...string) { } } -// Version() sends a CTCP "VERSION" to the target t +// Version sends a CTCP "VERSION" to the target nick or channel t. func (conn *Conn) Version(t string) { conn.Ctcp(t, VERSION) } -// Action() sends a CTCP "ACTION" to the target t +// Action sends a CTCP "ACTION" to the target nick or channel t. 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 +// Topic() sends a TOPIC command for a channel. +// If no topic is provided this requests that a 332 response is sent by the +// server for that channel, which can then be handled to retrieve the current +// channel topic. If a topic is provided the channel's topic will be set. +// TOPIC channel +// TOPIC channel :topic func (conn *Conn) Topic(channel string, topic ...string) { t := strings.Join(topic, " ") if t != "" { @@ -194,13 +215,14 @@ func (conn *Conn) Topic(channel string, topic ...string) { conn.Raw(TOPIC + " " + channel + t) } -// Mode() sends 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... +// Mode sends a MODE command for a target nick or channel t. +// If no mode strings are provided this requests that a 324 response is sent +// by the server for the target. Otherwise the mode strings are concatenated +// with spaces and sent to the server. This allows e.g. +// conn.Mode("#channel", "+nsk", "mykey") +// +// MODE t +// MODE t modestring func (conn *Conn) Mode(t string, modestring ...string) { mode := strings.Join(modestring, " ") if mode != "" { @@ -209,9 +231,11 @@ func (conn *Conn) Mode(t string, modestring ...string) { conn.Raw(MODE + " " + t + mode) } -// Away() sends an AWAY command to the server -// Away() resets away status -// Away(message) sets away with the given message +// Away sends an AWAY command to the server. +// If a message is provided it sets the client's away status with that message, +// otherwise it resets the client's away status. +// AWAY +// AWAY :message func (conn *Conn) Away(message ...string) { msg := strings.Join(message, " ") if msg != "" { @@ -220,20 +244,24 @@ func (conn *Conn) Away(message ...string) { conn.Raw(AWAY + msg) } -// Invite() sends an INVITE command to the server +// Invite sends an INVITE command to the server. +// INVITE nick channel func (conn *Conn) Invite(nick, channel string) { conn.Raw(INVITE + " " + nick + " " + channel) } -// Oper() sends an OPER command to the server +// Oper sends an OPER command to the server. +// OPER user pass func (conn *Conn) Oper(user, pass string) { conn.Raw(OPER + " " + user + " " + pass) } -// VHost() sends a VHOST command to the server +// VHost sends a VHOST command to the server. +// VHOST user pass func (conn *Conn) VHost(user, pass string) { conn.Raw(VHOST + " " + user + " " + pass) } -// Ping() sends a PING command to the server -// A PONG response is to be expected afterwards +// Ping sends a PING command to the server, which should PONG. +// PING :message func (conn *Conn) Ping(message string) { conn.Raw(PING + " :" + message) } -// Pong() sends a PONG command to the server +// Pong sends a PONG command to the server. +// PONG :message func (conn *Conn) Pong(message string) { conn.Raw(PONG + " :" + message) } diff --git a/client/connection.go b/client/connection.go index 511b157..903f7c6 100644 --- a/client/connection.go +++ b/client/connection.go @@ -13,7 +13,8 @@ import ( "time" ) -// An IRC connection is represented by this struct. +// Conn encapsulates a connection to a single IRC server. Create +// one with Client or SimpleClient. type Conn struct { // For preventing races on (dis)connect. mu sync.RWMutex @@ -47,54 +48,73 @@ type Conn struct { lastsent time.Time } -// Misc knobs to tweak client behaviour go in here +// Config contains options that can be passed to Client to change the +// behaviour of the library during use. It is recommended that NewConfig +// is used to create this struct rather than instantiating one directly. +// Passing a Config with no Nick in the Me field to Client will result +// in unflattering consequences. type Config struct { // Set this to provide the Nick, Ident and Name for the client to use. + // It is recommended to call Conn.Me to get up-to-date information + // about the current state of the client's IRC nick after connecting. Me *state.Nick // Hostname to connect to and optional connect password. + // Changing these after connection will have no effect until the + // client reconnects. Server, Pass string // Are we connecting via SSL? Do we care about certificate validity? + // Changing these after connection will have no effect until the + // client reconnects. SSL bool SSLConfig *tls.Config - // Local address to connect to the server. + // Local address to bind to when connecting to the server. LocalAddr string - // Replaceable function to customise the 433 handler's new nick + // Replaceable function to customise the 433 handler's new nick. + // By default an underscore "_" is appended to the current nick. NewNick func(string) string // Client->server ping frequency, in seconds. Defaults to 3m. + // Set to 0 to disable client-side pings. PingFreq time.Duration - // Set this to true to disable flood protection and false to re-enable + // The duration before a connection timeout is triggered. Defaults to 1m. + // Set to 0 to wait indefinitely. + Timeout time.Duration + + // Set this to true to disable flood protection and false to re-enable. Flood bool - // Sent as the reply to a CTCP VERSION message + // Sent as the reply to a CTCP VERSION message. Version string - // Sent as the QUIT message. + // Sent as the default QUIT message if Quit is called with no args. QuitMessage string // Configurable panic recovery for all handlers. + // Defaults to logging an error, see LogPanic. Recover func(*Conn, *Line) - // Split PRIVMSGs, NOTICEs and CTCPs longer than - // SplitLen characters over multiple lines. + // Split PRIVMSGs, NOTICEs and CTCPs longer than SplitLen characters + // over multiple lines. Default to 450 if not set. SplitLen int - // Timeout, The amount of time in seconds until a timeout is triggered. - Timeout time.Duration } +// NewConfig creates a Config struct containing sensible defaults. +// It takes one required argument: the nick to use for the client. +// Subsequent string arguments set the client's ident and "real" +// name, but these are optional. func NewConfig(nick string, args ...string) *Config { cfg := &Config{ Me: &state.Nick{Nick: nick}, PingFreq: 3 * time.Minute, NewNick: func(s string) string { return s + "_" }, Recover: (*Conn).LogPanic, // in dispatch.go - SplitLen: 450, + SplitLen: defaultSplit, Timeout: 60 * time.Second, } cfg.Me.Ident = "goirc" @@ -110,13 +130,16 @@ func NewConfig(nick string, args ...string) *Config { return cfg } -// 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 +// SimpleClient creates a new Conn, passing its arguments to NewConfig. +// If you don't need to change any client options and just want to get +// started quickly, this is a convenient shortcut. func SimpleClient(nick string, args ...string) *Conn { conn := Client(NewConfig(nick, args...)) return conn } +// Client takes a Config struct and returns a new Conn ready to have +// handlers added and connect to a server. func Client(cfg *Config) *Conn { if cfg == nil { cfg = NewConfig("__idiot__") @@ -157,16 +180,31 @@ func Client(cfg *Config) *Conn { return conn } +// Connected returns true if the client is successfully connected to +// an IRC server. It becomes true when the TCP connection is established, +// and false again when the connection is closed. func (conn *Conn) Connected() bool { conn.mu.RLock() defer conn.mu.RUnlock() return conn.connected } +// Config returns a pointer to the Config struct used by the client. +// Many of the elements of Config may be changed at any point to +// affect client behaviour. To disable flood protection temporarily, +// for example, a handler could do: +// +// conn.Config().Flood = true +// // Send many lines to the IRC server, risking "excess flood" +// conn.Config().Flood = false +// func (conn *Conn) Config() *Config { return conn.cfg } +// Me returns a state.Nick that reflects the client's IRC nick at the +// time it is called. If state tracking is enabled, this comes from +// the tracker, otherwise it is equivalent to conn.cfg.Me. func (conn *Conn) Me() *state.Nick { if conn.st != nil { conn.cfg.Me = conn.st.Me() @@ -174,10 +212,21 @@ func (conn *Conn) Me() *state.Nick { return conn.cfg.Me } +// StateTracker returns the state tracker being used by the client, +// if tracking is enabled, and nil otherwise. func (conn *Conn) StateTracker() state.Tracker { return conn.st } +// EnableStateTracking causes the client to track information about +// all channels it is joined to, and all the nicks in those channels. +// This can be rather handy for a number of bot-writing tasks. See +// the state package for more details. +// +// NOTE: Calling this while connected to an IRC server may cause the +// state tracker to become very confused all over STDERR if logging +// is enabled. State tracking should enabled before connecting or +// at a pinch while the client is not joined to any channels. func (conn *Conn) EnableStateTracking() { conn.mu.Lock() defer conn.mu.Unlock() @@ -190,6 +239,9 @@ func (conn *Conn) EnableStateTracking() { } } +// DisableStateTracking causes the client to stop tracking information +// about the channels and nicks it knows of. It will also wipe current +// state from the state tracker. func (conn *Conn) DisableStateTracking() { conn.mu.Lock() defer conn.mu.Unlock() @@ -211,11 +263,10 @@ 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 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. +// ConnectTo connects the IRC client to "host[:port]", which should be either +// a hostname or an IP address, with an optional port. It sets the client's +// Config.Server to host, Config.Pass to pass if one is provided, and then +// calls Connect. func (conn *Conn) ConnectTo(host string, pass ...string) error { conn.cfg.Server = host if len(pass) > 0 { @@ -224,6 +275,15 @@ func (conn *Conn) ConnectTo(host string, pass ...string) error { return conn.Connect() } +// Connect connects the IRC client to the server configured in Config.Server. +// To enable explicit SSL on the connection to the IRC server, set Config.SSL +// to true before calling Connect(). The port will default to 6697 if SSL is +// enabled, and 6667 otherwise. +// +// Upon successful connection, Connected will return true and a REGISTER event +// will be fired. This is mostly for internal use; it is suggested that a +// handler for the CONNECTED event is used to perform any initial client work +// like joining channels and sending messages. func (conn *Conn) Connect() error { conn.mu.Lock() defer conn.mu.Unlock() @@ -256,13 +316,13 @@ func (conn *Conn) Connect() error { return err } } - conn.connected = true conn.postConnect(true) + conn.connected = true conn.dispatch(&Line{Cmd: REGISTER, Time: time.Now()}) return nil } -// Post-connection setup (for ease of testing) +// postConnect performs post-connection setup, for ease of testing. func (conn *Conn) postConnect(start bool) { conn.io = bufio.NewReadWriter( bufio.NewReader(conn.sock), @@ -279,12 +339,15 @@ func (conn *Conn) postConnect(start bool) { } } -// copied from http.client for great justice +// hasPort returns true if the string hostname has a :port suffix. +// It was copied from net/http for great justice. func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") } -// goroutine to pass data from output channel to write() +// send is started as a goroutine after a connection is established. +// It shuttles data from the output channel to write(), and is killed +// when Conn.die is closed. func (conn *Conn) send() { for { select { @@ -304,7 +367,9 @@ func (conn *Conn) send() { } } -// receive one \r\n terminated line from peer, parse and dispatch it +// recv is started as a goroutine after a connection is established. +// It receives "\r\n" terminated lines from the server, parses them into +// Lines, and sends them to the input channel. func (conn *Conn) recv() { for { s, err := conn.io.ReadString('\n') @@ -329,7 +394,8 @@ func (conn *Conn) recv() { } } -// Repeatedly pings the server every PingFreq seconds (no matter what) +// ping is started as a goroutine after a connection is established, as +// long as Config.PingFreq >0. It pings the server every PingFreq seconds. func (conn *Conn) ping() { defer conn.wg.Done() tick := time.NewTicker(conn.cfg.PingFreq) @@ -345,7 +411,9 @@ func (conn *Conn) ping() { } } -// goroutine to dispatch events for lines received on input channel +// runLoop is started as a goroutine after a connection is established. +// It pulls Lines from the input channel and dispatches them to any +// handlers that have been registered for that IRC verb. func (conn *Conn) runLoop() { defer conn.wg.Done() for { @@ -359,7 +427,7 @@ func (conn *Conn) runLoop() { } } -// Write a \r\n terminated line of output to the connected server, +// write writes a \r\n terminated line of output to the connected server, // using Hybrid's algorithm to rate limit if conn.cfg.Flood is false. func (conn *Conn) write(line string) error { if !conn.cfg.Flood { @@ -381,7 +449,7 @@ func (conn *Conn) write(line string) error { return nil } -// Implement Hybrid's flood control algorithm to rate-limit outgoing lines. +// rateLimit implements Hybrid's flood control algorithm for outgoing lines. func (conn *Conn) rateLimit(chars int) time.Duration { // Hybrid's algorithm allows for 2 seconds per line and an additional // 1/120 of a second per character on that line. @@ -400,6 +468,8 @@ func (conn *Conn) rateLimit(chars int) time.Duration { return 0 } +// shutdown tears down all connection-related state. It is called when either +// the sending or receiving goroutines encounter an error. func (conn *Conn) shutdown() { // Guard against double-call of shutdown() if we get an error in send() // as calling sock.Close() will cause recv() to receive EOF in readstring() @@ -416,7 +486,7 @@ func (conn *Conn) shutdown() { conn.mu.Unlock() // Dispatch after closing connection but before reinit // so event handlers can still access state information. - defer conn.dispatch(&Line{Cmd: DISCONNECTED, Time: time.Now()}) + conn.dispatch(&Line{Cmd: DISCONNECTED, Time: time.Now()}) } // Dumps a load of information about the current state of the connection to a diff --git a/client/dispatch.go b/client/dispatch.go index b40dd0b..2084e74 100644 --- a/client/dispatch.go +++ b/client/dispatch.go @@ -7,17 +7,32 @@ import ( "sync" ) -// An IRC Handler looks like this: +// 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). +// +// Foreground handlers have a guarantee of protocol consistency: all the +// handlers for one event will have finished before the handlers for the +// next start processing. They are run in parallel but block the event +// loop, so care should be taken to ensure these handlers are quick :-) +// +// Background handlers are run in parallel and do not block the event loop. +// This is useful for things that may need to do significant work. type Handler interface { Handle(*Conn, *Line) } -// And when they've been added to the client they are removable. +// Removers allow for a handler that has been previously added to the client +// to be removed. type Remover interface { Remove() } -// A HandlerFunc implements Handler. +// HandlerFunc allows a bare function with this signature to implement the +// Handler interface. It is used by Conn.HandleFunc. type HandlerFunc func(*Conn, *Line) func (hf HandlerFunc) Handle(conn *Conn, line *Line) { @@ -132,16 +147,15 @@ func (hs *hSet) dispatch(conn *Conn, line *Line) { wg.Wait() } -// 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). +// Handle adds the provided handler to the foreground set for the named event. +// It will return a Remover that allows that handler to be removed again. func (conn *Conn) Handle(name string, h Handler) Remover { return conn.fgHandlers.add(name, h) } +// HandleBG adds the provided handler to the background set for the named +// event. It may go away in the future. +// It will return a Remover that allows that handler to be removed again. func (conn *Conn) HandleBG(name string, h Handler) Remover { return conn.bgHandlers.add(name, h) } @@ -150,6 +164,9 @@ func (conn *Conn) handle(name string, h Handler) Remover { return conn.intHandlers.add(name, h) } +// HandleFunc adds the provided function as a handler in the foreground set +// for the named event. +// It will return a Remover that allows that handler to be removed again. func (conn *Conn) HandleFunc(name string, hf HandlerFunc) Remover { return conn.Handle(name, hf) } @@ -159,16 +176,14 @@ func (conn *Conn) dispatch(line *Line) { // This ensures that user-supplied handlers that use the tracker have a // consistent view of the connection state in handlers that mutate it. conn.intHandlers.dispatch(conn, line) - // Background handlers are run in parallel and do not block the event loop. - // This is useful for things that may need to do significant work. go conn.bgHandlers.dispatch(conn, line) - // Foreground handlers have a guarantee of protocol consistency: all the - // handlers for one event will have finished before the handlers for the - // next start processing. They are run in parallel but block the event - // loop, so care should be taken to ensure these handlers are quick :-) conn.fgHandlers.dispatch(conn, line) } +// LogPanic is used as the default panic catcher for the client. If, like me, +// you are not good with computer, and you'd prefer your bot not to vanish into +// the ether whenever you make unfortunate programming mistakes, you may find +// this useful: it will recover panics from handler code and log the errors. func (conn *Conn) LogPanic(line *Line) { if err := recover(); err != nil { _, f, l, _ := runtime.Caller(2) diff --git a/client/doc.go b/client/doc.go new file mode 100644 index 0000000..3f394c7 --- /dev/null +++ b/client/doc.go @@ -0,0 +1,34 @@ +// Package client implements an IRC client. It handles protocol basics +// such as initial connection and responding to server PINGs, and has +// optional state tracking support which will keep tabs on every nick +// present in the same channels as the client. Other features include +// SSL support, automatic splitting of long lines, and panic recovery +// for handlers. +// +// Incoming IRC messages are parsed into client.Line structs and trigger +// events based on the IRC verb (e.g. PRIVMSG) of the message. Handlers +// for these events conform to the client.Handler interface; a HandlerFunc +// type to wrap bare functions is provided a-la the net/http package. +// +// Creating a client, adding a handler and connecting to a server looks +// soemthing like this, for the simple case: +// +// // Create a new client, which will connect with the nick "myNick" +// irc := client.SimpleClient("myNick") +// +// // Add a handler that waits for the "disconnected" event and +// // closes a channel to signal everything is done. +// disconnected := make(chan struct{}) +// c.HandleFunc("disconnected", func(c *client.Conn, l *client.Line) { +// close(disconnected) +// }) +// +// // Connect to an IRC server. +// if err := c.ConnectTo("irc.freenode.net"); err != nil { +// log.Fatalf("Connection error: %v\n", err) +// } +// +// // Wait for disconnection. +// <-disconnected +// +package client diff --git a/client/line.go b/client/line.go index 65b4155..1877196 100644 --- a/client/line.go +++ b/client/line.go @@ -6,7 +6,8 @@ import ( ) // 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. +// name for incoming event handlers and is the IRC verb, the first sequence +// of non-whitespace characters after ":nick!user@host", e.g. PRIVMSG. // Raw =~ ":nick!user@host cmd args[] :text" // Src == "nick!user@host" // Cmd == e.g. PRIVMSG, 332 @@ -17,7 +18,7 @@ type Line struct { Time time.Time } -// Copy() returns a deep copy of the Line. +// Copy returns a deep copy of the Line. func (l *Line) Copy() *Line { nl := *l nl.Args = make([]string, len(l.Args)) @@ -25,7 +26,7 @@ func (l *Line) Copy() *Line { return &nl } -// Return the contents of the text portion of a line. This only really +// Text returns the contents of the text portion of a line. This only really // makes sense for lines with a :text part, but there are a lot of them. func (line *Line) Text() string { if len(line.Args) > 0 { @@ -34,11 +35,12 @@ func (line *Line) Text() string { return "" } -// Return the target of the line, usually the first Arg for the IRC verb. -// If the line was broadcast from a channel, the target will be that channel. -// If the line was broadcast by a user, the target will be that user. -// TODO(fluffle): Add 005 CHANTYPES parsing for this? +// Target returns the contextual target of the line, usually the first Arg +// for the IRC verb. If the line was broadcast from a channel, the target +// will be that channel. If the line was sent directly by a user, the target +// will be that user. func (line *Line) Target() string { + // TODO(fluffle): Add 005 CHANTYPES parsing for this? switch line.Cmd { case PRIVMSG, NOTICE, ACTION: if !line.Public() { @@ -56,7 +58,11 @@ func (line *Line) Target() string { return "" } -// NOTE: Makes the assumption that all channels start with #. +// Public returns true if the line is the result of an IRC user sending +// a message to a channel the client has joined instead of directly +// to the client. +// +// NOTE: This makes the (poor) assumption that all channels start with #. func (line *Line) Public() bool { switch line.Cmd { case PRIVMSG, NOTICE, ACTION: @@ -78,7 +84,13 @@ func (line *Line) Public() bool { } -// ParseLine() creates a Line from an incoming message from the IRC server. +// ParseLine creates a Line from an incoming message from the IRC server. +// +// It contains special casing for CTCP messages, most notably CTCP ACTION. +// All CTCP messages have the \001 bytes stripped from the message and the +// CTCP command separated from any subsequent text. Then, CTCP ACTIONs are +// rewritten such that Line.Cmd == ACTION. Other CTCP messages have Cmd +// set to CTCP or CTCPREPLY, and the CTCP command prepended to line.Args. func ParseLine(s string) *Line { line := &Line{Raw: s} if s[0] == ':' {