mirror of
				https://github.com/fluffle/goirc
				synced 2025-10-31 10:18:04 +00:00 
			
		
		
		
	
						commit
						d9267067cd
					
				
					 6 changed files with 6 additions and 6 deletions
				
			
		
							
								
								
									
										14
									
								
								client/Makefile
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								client/Makefile
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| # Copyright 2009 The Go Authors. All rights reserved.
 | ||||
| # Use of this source code is governed by a BSD-style
 | ||||
| # license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| include $(GOROOT)/src/Make.inc | ||||
| 
 | ||||
| TARG=github.com/fluffle/goirc/client | ||||
| GOFILES=\
 | ||||
| 	connection.go\
 | ||||
| 	commands.go\
 | ||||
| 	handlers.go\
 | ||||
| 	nickchan.go | ||||
| 
 | ||||
| include $(GOROOT)/src/Make.pkg | ||||
							
								
								
									
										140
									
								
								client/commands.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								client/commands.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,140 @@ | |||
| package client | ||||
| 
 | ||||
| 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. | ||||
| 
 | ||||
| // 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 } | ||||
| 
 | ||||
| // Pass() sends a PASS command to the server | ||||
| func (conn *Conn) Pass(password string) { conn.out <- "PASS "+password } | ||||
| 
 | ||||
| // Nick() sends a NICK command to the server | ||||
| func (conn *Conn) Nick(nick string) { conn.out <- "NICK "+nick } | ||||
| 
 | ||||
| // User() sends a USER command to the server | ||||
| func (conn *Conn) User(ident, name string) { | ||||
| 	conn.out <- "USER "+ident+" 12 * :"+name | ||||
| } | ||||
| 
 | ||||
| // Join() sends a JOIN command to the server | ||||
| 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 := strings.Join(message, " ") | ||||
| 	if msg != "" { | ||||
| 		msg = " :" + msg | ||||
| 	} | ||||
| 	conn.out <- "PART "+channel+msg | ||||
| } | ||||
| 
 | ||||
| // Kick() sends a KICK command to remove a nick from a channel | ||||
| func (conn *Conn) Kick(channel, nick string, message ...string) { | ||||
| 	msg := strings.Join(message, " ") | ||||
| 	if msg != "" { | ||||
| 		msg = " :" + msg | ||||
| 	} | ||||
| 	conn.out <- "KICK "+channel+" "+nick+msg | ||||
| } | ||||
| 
 | ||||
| // Quit() sends a QUIT command to the server with an optional quit message | ||||
| func (conn *Conn) Quit(message ...string) { | ||||
| 	msg := strings.Join(message, " ") | ||||
| 	if msg == "" { | ||||
| 		msg = "GoBye!" | ||||
| 	} | ||||
| 	conn.out <- "QUIT :"+msg | ||||
| } | ||||
| 
 | ||||
| // Whois() sends a WHOIS command to the server | ||||
| func (conn *Conn) Whois(nick string) { conn.out <- "WHOIS "+nick } | ||||
| 
 | ||||
| //Who() sends a WHO command to the server | ||||
| func (conn *Conn) Who(nick string) { conn.out <- "WHO "+nick } | ||||
| 
 | ||||
| // Privmsg() sends a PRIVMSG to the target t | ||||
| func (conn *Conn) Privmsg(t, msg string) { conn.out <- "PRIVMSG "+t+" :"+msg } | ||||
| 
 | ||||
| // Notice() sends a NOTICE to the target t | ||||
| func (conn *Conn) Notice(t, msg string) { conn.out <- "NOTICE "+t+" :"+msg } | ||||
| 
 | ||||
| // Ctcp() sends a (generic) CTCP message to the target t | ||||
| // with an optional argument | ||||
| func (conn *Conn) Ctcp(t, ctcp string, arg ...string) { | ||||
| 	msg := strings.Join(arg, " ") | ||||
| 	if msg != "" { | ||||
| 		msg = " " + msg | ||||
| 	} | ||||
| 	conn.Privmsg(t, "\001"+ctcp+msg+"\001") | ||||
| } | ||||
| 
 | ||||
| // CtcpReply() sends a generic CTCP reply to the target t | ||||
| // with an optional argument | ||||
| func (conn *Conn) CtcpReply(t, ctcp string, arg ...string) { | ||||
| 	msg := strings.Join(arg, " ") | ||||
| 	if msg != "" { | ||||
| 		msg = " " + msg | ||||
| 	} | ||||
| 	conn.Notice(t, "\001"+ctcp+msg+"\001") | ||||
| } | ||||
| 
 | ||||
| // Version() sends a CTCP "VERSION" to the target t | ||||
| 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) } | ||||
| 
 | ||||
| // 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 := strings.Join(topic, " ") | ||||
| 	if t != "" { | ||||
| 		t = " :" + t | ||||
| 	} | ||||
| 	conn.out <- "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 <nick>" or "+ntk <key>" 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 := strings.Join(modestring, " ") | ||||
| 	if mode != "" { | ||||
| 		mode = " " + mode | ||||
| 	} | ||||
| 	conn.out <- "MODE "+t+mode | ||||
| } | ||||
| 
 | ||||
| // 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 := strings.Join(message, " ") | ||||
| 	if msg != "" { | ||||
| 		msg = " :" + msg | ||||
| 	} | ||||
| 	conn.out <- "AWAY"+msg | ||||
| } | ||||
| 
 | ||||
| // Invite() sends an INVITE command to the server | ||||
| func (conn *Conn) Invite(nick, channel string) { | ||||
| 	conn.out <- "INVITE "+nick+" "+channel | ||||
| } | ||||
| 
 | ||||
| // Oper() sends an OPER command to the server | ||||
| func (conn *Conn) Oper(user, pass string) { | ||||
| 	conn.out <- "OPER "+user+" "+pass | ||||
| } | ||||
							
								
								
									
										295
									
								
								client/connection.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										295
									
								
								client/connection.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,295 @@ | |||
| package client | ||||
| 
 | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"os" | ||||
| 	"net" | ||||
| 	"crypto/tls" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| // 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 { | ||||
| 	// Connection Hostname and Nickname | ||||
| 	Host    string | ||||
| 	Me      *Nick | ||||
| 	Network string | ||||
| 
 | ||||
| 	// I/O stuff to server | ||||
| 	sock      net.Conn | ||||
| 	io        *bufio.ReadWriter | ||||
| 	in        chan *Line | ||||
| 	out       chan string | ||||
| 	connected 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 | ||||
| 
 | ||||
| 	// Set this to true to disable flood protection and false to re-enable | ||||
| 	Flood bool | ||||
| 
 | ||||
| 	Debug bool | ||||
| 
 | ||||
| 	// Event handler mapping | ||||
| 	events map[string][]func(*Conn, *Line) | ||||
| 
 | ||||
| 	// Map of channels we're on | ||||
| 	chans map[string]*Channel | ||||
| 
 | ||||
| 	// Map of nicks we know about | ||||
| 	nicks map[string]*Nick | ||||
| } | ||||
| 
 | ||||
| // 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 | ||||
| } | ||||
| 
 | ||||
| // 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() | ||||
| 	conn.Me = conn.NewNick(nick, user, name, "") | ||||
| 	conn.setupEvents() | ||||
| 	return conn | ||||
| } | ||||
| 
 | ||||
| func (conn *Conn) initialise() { | ||||
| 	// allocate meh some memoraaaahh | ||||
| 	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.SSL = false | ||||
| 	conn.SSLConfig = nil | ||||
| 	conn.io = nil | ||||
| 	conn.sock = nil | ||||
| 
 | ||||
| 	// if this is being called because we are reconnecting, conn.Me | ||||
| 	// will still have all the old channels referenced -- nuke them! | ||||
| 	if conn.Me != nil { | ||||
| 		conn.Me = conn.NewNick(conn.Me.Nick, conn.Me.Ident, conn.Me.Name, "") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // 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. | ||||
| 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)) | ||||
| 	} | ||||
| 
 | ||||
| 	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 | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	conn.Host = host | ||||
| 	conn.io = bufio.NewReadWriter( | ||||
| 		bufio.NewReader(conn.sock), | ||||
| 		bufio.NewWriter(conn.sock)) | ||||
| 	go conn.send() | ||||
| 	go conn.recv() | ||||
| 
 | ||||
| 	if len(pass) > 0 { | ||||
| 		conn.Pass(pass[0]) | ||||
| 	} | ||||
| 	conn.Nick(conn.Me.Nick) | ||||
| 	conn.User(conn.Me.Ident, conn.Me.Name) | ||||
| 
 | ||||
| 	go conn.runLoop() | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // dispatch a nicely formatted os.Error to the error channel | ||||
| func (conn *Conn) error(s string, a ...interface{}) { | ||||
| 	conn.Err <- os.NewError(fmt.Sprintf(s, a...)) | ||||
| } | ||||
| 
 | ||||
| // copied from http.client for great justice | ||||
| 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 | ||||
| 	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. | ||||
| 		linetime = 2*second + int64(len(line))*second/120 | ||||
| 		if !conn.Flood && conn.connected { | ||||
| 			// No point in tallying up flood protection stuff until connected | ||||
| 			if badness += linetime + lastsent - time.Nanoseconds(); badness < 0 { | ||||
| 				// negative badness times are badness... | ||||
| 				badness = int64(0) | ||||
| 			} | ||||
| 		} | ||||
| 		lastsent = time.Nanoseconds() | ||||
| 
 | ||||
| 		// If we've sent more than 10 second's worth of lines according to the | ||||
| 		// calculation above, then we're at risk of "Excess Flood". | ||||
| 		if badness > 10*second && !conn.Flood { | ||||
| 			// 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 { | ||||
| 			conn.error("irc.send(): %s", err.String()) | ||||
| 			conn.shutdown() | ||||
| 			break | ||||
| 		} | ||||
| 		conn.io.Flush() | ||||
| 		if conn.Debug { | ||||
| 			fmt.Println("-> " + line) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // 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 | ||||
| 		} | ||||
| 		s = strings.Trim(s, "\r\n") | ||||
| 		if conn.Debug { | ||||
| 			fmt.Println("<- " + s) | ||||
| 		} | ||||
| 
 | ||||
| 		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)] | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// 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.Fields(args[0]) | ||||
| 		line.Cmd = strings.ToUpper(args[0]) | ||||
| 		if len(args) > 1 { | ||||
| 			line.Args = args[1:len(args)] | ||||
| 			// some servers (Gamesurge) don't use : properly | ||||
| 			// so duplicate Args[0] into Text | ||||
| 			if line.Text == "" { | ||||
| 				line.Text = args[len(args)-1] | ||||
| 			} | ||||
| 		} else { | ||||
| 			// now duplicate Text into Args[0] if no args | ||||
| 			line.Args = []string{line.Text} | ||||
| 		} | ||||
| 		conn.in <- line | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (conn *Conn) runLoop() { | ||||
| 	for line := range conn.in { | ||||
| 		conn.dispatchEvent(line) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (conn *Conn) shutdown() { | ||||
| 	close(conn.in) | ||||
| 	close(conn.out) | ||||
| 	close(conn.Err) | ||||
| 	conn.connected = false | ||||
| 	conn.sock.Close() | ||||
| 	// reinit datastructures ready for next connection | ||||
| 	// do this here rather than after runLoop()'s for due to race | ||||
| 	conn.initialise() | ||||
| } | ||||
| 
 | ||||
| // 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" | ||||
| 	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 | ||||
| } | ||||
							
								
								
									
										369
									
								
								client/handlers.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										369
									
								
								client/handlers.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,369 @@ | |||
| package client | ||||
| 
 | ||||
| // this file contains the basic set of event handlers | ||||
| // to manage tracking an irc connection etc. | ||||
| 
 | ||||
| import "strings" | ||||
| 
 | ||||
| // 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 { | ||||
| 		conn.events[n] = append(e, f) | ||||
| 	} else { | ||||
| 		e := make([]func(*Conn, *Line), 1, 10) | ||||
| 		e[0] = f | ||||
| 		conn.events[n] = e | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // loops through all event handlers for line.Cmd, running each in a goroutine | ||||
| func (conn *Conn) dispatchEvent(line *Line) { | ||||
| 	// seems that we end up dispatching an event with a nil line when receiving | ||||
| 	// EOF from the server. Until i've tracked down why.... | ||||
| 	if line == nil { | ||||
| 		conn.error("irc.dispatchEvent(): buh? line == nil :-(") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// So, I think CTCP and (in particular) CTCP ACTION are better handled as | ||||
| 	// separate events as opposed to forcing people to have gargantuan PRIVMSG | ||||
| 	// handlers to cope with the possibilities. | ||||
| 	if line.Cmd == "PRIVMSG" && len(line.Text) > 2 && | ||||
| 		line.Text[0] == '\001' && line.Text[len(line.Text)-1] == '\001' { | ||||
| 		// WOO, it's a CTCP message | ||||
| 		t := strings.Split(line.Text[1:len(line.Text)-1], " ", 2) | ||||
| 		if c := strings.ToUpper(t[0]); c == "ACTION" { | ||||
| 			// make a CTCP ACTION it's own event a-la PRIVMSG | ||||
| 			line.Cmd = c | ||||
| 		} else { | ||||
| 			// otherwise, dispatch a generic CTCP event that | ||||
| 			// contains the type of CTCP in line.Args[0] | ||||
| 			line.Cmd = "CTCP" | ||||
| 			a := make([]string, len(line.Args)+1) | ||||
| 			a[0] = c | ||||
| 			for i := 0; i < len(line.Args); i++ { | ||||
| 				a[i+1] = line.Args[i] | ||||
| 			} | ||||
| 			line.Args = a | ||||
| 		} | ||||
| 		if len(t) > 1 { | ||||
| 			// for some CTCP messages this could make more sense | ||||
| 			// in line.Args[], but meh. MEH, I say. | ||||
| 			line.Text = t[1] | ||||
| 		} | ||||
| 	} | ||||
| 	if funcs, ok := conn.events[line.Cmd]; ok { | ||||
| 		for _, f := range funcs { | ||||
| 			go f(conn, line) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Basic ping/pong handler | ||||
| func (conn *Conn) h_PING(line *Line) { | ||||
| 	conn.Raw("PONG :" + line.Text) | ||||
| } | ||||
| 
 | ||||
| // Handler to trigger a "CONNECTED" event on receipt of numeric 001 | ||||
| func (conn *Conn) h_001(line *Line) { | ||||
| 	// we're connected! | ||||
| 	conn.connected = true | ||||
| 	conn.dispatchEvent(&Line{Cmd: "CONNECTED"}) | ||||
| 	// and we're being given our hostname (from the server's perspective) | ||||
| 	if ridx := strings.LastIndex(line.Text, " "); ridx != -1 { | ||||
| 		h := line.Text[ridx+1 : len(line.Text)] | ||||
| 		if idx := strings.Index(h, "@"); idx != -1 { | ||||
| 			conn.Me.Host = h[idx+1 : len(h)] | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // XXX: do we need 005 protocol support message parsing here? | ||||
| // probably in the future, but I can't quite be arsed yet. | ||||
| /* | ||||
| 	:irc.pl0rt.org 005 GoTest CMDS=KNOCK,MAP,DCCALLOW,USERIP UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=20 CHANLIMIT=#:20 MAXLIST=b:60,e:60,I:60 NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 :are supported by this server | ||||
| 	:irc.pl0rt.org 005 GoTest MAXTARGETS=20 WALLCHOPS WATCH=128 WATCHOPTS=A SILENCE=15 MODES=12 CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beI,kfL,lj,psmntirRcOAQKVCuzNSMT NETWORK=bb101.net CASEMAPPING=ascii EXTBAN=~,cqnr ELIST=MNUCT :are supported by this server | ||||
| 	:irc.pl0rt.org 005 GoTest STATUSMSG=~&@%+ EXCEPTS INVEX :are supported by this server | ||||
| */ | ||||
| 
 | ||||
| // Handler to deal with "433 :Nickname already in use" | ||||
| func (conn *Conn) h_433(line *Line) { | ||||
| 	// Args[1] is the new nick we were attempting to acquire | ||||
| 	conn.Nick(line.Args[1] + "_") | ||||
| 	// if this is happening before we're properly connected (i.e. the nick | ||||
| 	// we sent in the initial NICK command is in use) we will not receive | ||||
| 	// a NICK message to confirm our change of nick, so ReNick here... | ||||
| 	if !conn.connected && line.Args[1] == conn.Me.Nick { | ||||
| 		conn.Me.ReNick(line.Args[1] + "_") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Handler NICK messages to inform us about nick changes | ||||
| func (conn *Conn) h_NICK(line *Line) { | ||||
| 	// all nicks should be handled the same way, our own included | ||||
| 	if n := conn.GetNick(line.Nick); n != nil { | ||||
| 		n.ReNick(line.Text) | ||||
| 	} else { | ||||
| 		conn.error("irc.NICK(): buh? unknown nick %s.", line.Nick) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Handle VERSION requests and CTCP PING | ||||
| func (conn *Conn) h_CTCP(line *Line) { | ||||
| 	if line.Args[0] == "VERSION" { | ||||
| 		conn.CtcpReply(line.Nick, "VERSION", "powered by goirc...") | ||||
| 	} else if line.Args[0] == "PING" { | ||||
| 		conn.CtcpReply(line.Nick, "PING", line.Text) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Handle JOINs to channels to maintain state | ||||
| func (conn *Conn) h_JOIN(line *Line) { | ||||
| 	// Some IRCds (ircu) send ':n!u@h JOIN #chan' not ':n!u@h JOIN :#chan' | ||||
| 	// Unfortunately the RFCs aren't specific about this. In fact the | ||||
| 	// examples indicate no colon should be sent, but it's unusual. | ||||
| 	var chname string | ||||
| 	if len(line.Text) > 0 { | ||||
| 		chname = line.Text | ||||
| 	} else if len(line.Args) > 0 { | ||||
| 		chname = line.Args[0] | ||||
| 	} | ||||
| 	ch := conn.GetChannel(chname) | ||||
| 	n := conn.GetNick(line.Nick) | ||||
| 	if ch == nil { | ||||
| 		// first we've seen of this channel, so should be us joining it | ||||
| 		// NOTE this will also take care of n == nil && ch == nil | ||||
| 		if n != conn.Me { | ||||
| 			conn.error("irc.JOIN(): buh? JOIN to unknown channel %s recieved from (non-me) nick %s", line.Text, line.Nick) | ||||
| 			return | ||||
| 		} | ||||
| 		ch = conn.NewChannel(chname) | ||||
| 		// since we don't know much about this channel, ask server for info | ||||
| 		// we get the channel users automatically in 353 and the channel | ||||
| 		// topic in 332 on join, so we just need to get the modes | ||||
| 		conn.Mode(ch.Name) | ||||
| 		// sending a WHO for the channel is MUCH more efficient than | ||||
| 		// triggering a WHOIS on every nick from the 353 handler | ||||
| 		conn.Who(ch.Name) | ||||
| 	} | ||||
| 	if n == nil { | ||||
| 		// this is the first we've seen of this nick | ||||
| 		n = conn.NewNick(line.Nick, line.Ident, "", line.Host) | ||||
| 		// since we don't know much about this nick, ask server for info | ||||
| 		conn.Who(n.Nick) | ||||
| 	} | ||||
| 	// this takes care of both nick and channel linking \o/ | ||||
| 	ch.AddNick(n) | ||||
| } | ||||
| 
 | ||||
| // Handle PARTs from channels to maintain state | ||||
| func (conn *Conn) h_PART(line *Line) { | ||||
| 	// Some IRCds (ircu) send 'PART :#chan' when there's no part message | ||||
| 	// instead of 'PART #chan'. This is *questionable* behaviour... | ||||
| 	var chname string | ||||
| 	if len(line.Args) > 0 { | ||||
| 		chname = line.Args[0] | ||||
| 	} else if len(line.Text) > 0 { | ||||
| 		chname = line.Text | ||||
| 	} | ||||
| 	ch := conn.GetChannel(chname) | ||||
| 	n := conn.GetNick(line.Nick) | ||||
| 	if ch != nil && n != nil { | ||||
| 		ch.DelNick(n) | ||||
| 	} else { | ||||
| 		conn.error("irc.PART(): buh? PART of channel %s by nick %s", chname, line.Nick) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Handle KICKs from channels to maintain state | ||||
| func (conn *Conn) h_KICK(line *Line) { | ||||
| 	// XXX: this won't handle autorejoining channels on KICK | ||||
| 	// it's trivial to do this in a seperate handler... | ||||
| 	ch := conn.GetChannel(line.Args[0]) | ||||
| 	n := conn.GetNick(line.Args[1]) | ||||
| 	if ch != nil && n != nil { | ||||
| 		ch.DelNick(n) | ||||
| 	} else { | ||||
| 		conn.error("irc.KICK(): buh? KICK from channel %s of nick %s", line.Args[0], line.Args[1]) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Handle other people's QUITs | ||||
| func (conn *Conn) h_QUIT(line *Line) { | ||||
| 	if n := conn.GetNick(line.Nick); n != nil { | ||||
| 		n.Delete() | ||||
| 	} else { | ||||
| 		conn.error("irc.QUIT(): buh? QUIT from unknown nick %s", line.Nick) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Handle MODE changes for channels we know about (and our nick personally) | ||||
| 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 "<hop count> <real name>" | ||||
| 		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) | ||||
| } | ||||
							
								
								
									
										14
									
								
								client/irc_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								client/irc_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| package client | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| // Not really sure what or how to test something that basically requires a | ||||
| // connection to an IRC server to function, but we need some tests or when this | ||||
| // is present in the go package tree builds fail hard :-( | ||||
| func TestIRC(t *testing.T) { | ||||
| 	if c := New("test", "test", "Testing IRC"); c == nil { | ||||
| 		t.FailNow() | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										507
									
								
								client/nickchan.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										507
									
								
								client/nickchan.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,507 @@ | |||
| package client | ||||
| 
 | ||||
| // Here you'll find the Channel and Nick structs | ||||
| // as well as the internal state maintenance code for the handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| // A struct representing an IRC channel | ||||
| type Channel struct { | ||||
| 	Name, Topic string | ||||
| 	Modes       *ChanMode | ||||
| 	Nicks       map[*Nick]*ChanPrivs | ||||
| 	Bans        map[string]string | ||||
| 	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 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 { | ||||
| 	// 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 | ||||
| \******************************************************************************/ | ||||
| 
 | ||||
| // 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() | ||||
| 	conn.nicks[n.Nick] = n | ||||
| 	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 | ||||
| 	} | ||||
| 	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() | ||||
| 	conn.chans[ch.Name] = ch | ||||
| 	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 | ||||
| 	} | ||||
| 	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 | ||||
| \******************************************************************************/ | ||||
| 
 | ||||
| func (ch *Channel) initialise() { | ||||
| 	ch.Modes = new(ChanMode) | ||||
| 	ch.Nicks = make(map[*Nick]*ChanPrivs) | ||||
| 	ch.Bans = make(map[string]string) | ||||
| } | ||||
| 
 | ||||
| // 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) | ||||
| 		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) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // 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 { | ||||
| 		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) AddBan(nick, ban string) { | ||||
| 	ch.Bans[nick] = ban | ||||
| } | ||||
| 
 | ||||
| func (ch *Channel) DeleteBan(ban string) { | ||||
| 	for n, b := range ch.Bans { | ||||
| 		if b == ban { | ||||
| 			ch.Bans[n] = "", false // see go issue 1249 | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // 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() { | ||||
| 	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) | ||||
| } | ||||
| 
 | ||||
| // 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) | ||||
| 		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) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // 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 { | ||||
| 		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() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // 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 { | ||||
| 		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. | ||||
| \******************************************************************************/ | ||||
| 
 | ||||
| // Map *irc.ChanMode fields to IRC mode characters | ||||
| 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", | ||||
| } | ||||
| 
 | ||||
| // Map *irc.NickMode fields to IRC mode characters | ||||
| var NickModeToString = map[string]string{ | ||||
| 	"Invisible":  "i", | ||||
| 	"Oper":       "o", | ||||
| 	"WallOps":    "w", | ||||
| 	"HiddenHost": "x", | ||||
| 	"SSL":        "z", | ||||
| } | ||||
| 
 | ||||
| // Map *irc.ChanPrivs fields to IRC mode characters | ||||
| var ChanPrivToString = map[string]string{ | ||||
| 	"Owner":  "q", | ||||
| 	"Admin":  "a", | ||||
| 	"Op":     "o", | ||||
| 	"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":  '&', | ||||
| 	"Op":     '@', | ||||
| 	"HalfOp": '%', | ||||
| 	"Voice":  '+', | ||||
| } | ||||
| 
 | ||||
| // Reverse mappings of the above datastructures | ||||
| 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 | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Returns a string representing the channel. Looks like: | ||||
| //	Channel: <channel name> e.g. #moo | ||||
| //	Topic: <channel topic> e.g. Discussing the merits of cows! | ||||
| //	Mode: <channel modes> e.g. +nsti | ||||
| //	Nicks: | ||||
| //		<nick>: <privs> e.g. CowMaster: +o | ||||
| //		... | ||||
| 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 | ||||
| } | ||||
| 
 | ||||
| // Returns a string representing the nick. Looks like: | ||||
| //	Nick: <nick name> e.g. CowMaster | ||||
| //	Hostmask: <ident@host> e.g. moo@cows.org | ||||
| //	Real Name: <real name> e.g. Steve "CowMaster" Bush | ||||
| //	Modes: <nick modes> e.g. +z | ||||
| //	Channels: | ||||
| //		<channel>: <privs> e.g. #moo: +o | ||||
| //		... | ||||
| 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 | ||||
| } | ||||
| 
 | ||||
| // Returns a string representing the channel modes. Looks like: | ||||
| //	+npk key | ||||
| 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", f.Get()) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	for _, s := range a { | ||||
| 		if s != "" { | ||||
| 			str += " " + s | ||||
| 		} | ||||
| 	} | ||||
| 	if str == "+" { | ||||
| 		str = "No modes set" | ||||
| 	} | ||||
| 	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) | ||||
| 	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 | ||||
| } | ||||
| 
 | ||||
| // Returns a string representing the channel privileges. Looks like: | ||||
| //	+o | ||||
| 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 | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue