519 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			519 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
 | |
| // of this source code is governed by the MIT license that can be found in
 | |
| // the LICENSE file.
 | |
| 
 | |
| package girc
 | |
| 
 | |
| import (
 | |
| 	"strings"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| // registerBuiltin sets up built-in handlers, based on client
 | |
| // configuration.
 | |
| func (c *Client) registerBuiltins() {
 | |
| 	c.debug.Print("registering built-in handlers")
 | |
| 	c.Handlers.mu.Lock()
 | |
| 
 | |
| 	// Built-in things that should always be supported.
 | |
| 	c.Handlers.register(true, true, RPL_WELCOME, HandlerFunc(handleConnect))
 | |
| 	c.Handlers.register(true, false, PING, HandlerFunc(handlePING))
 | |
| 	c.Handlers.register(true, false, PONG, HandlerFunc(handlePONG))
 | |
| 
 | |
| 	if !c.Config.disableTracking {
 | |
| 		// Joins/parts/anything that may add/remove/rename users.
 | |
| 		c.Handlers.register(true, false, JOIN, HandlerFunc(handleJOIN))
 | |
| 		c.Handlers.register(true, false, PART, HandlerFunc(handlePART))
 | |
| 		c.Handlers.register(true, false, KICK, HandlerFunc(handleKICK))
 | |
| 		c.Handlers.register(true, false, QUIT, HandlerFunc(handleQUIT))
 | |
| 		c.Handlers.register(true, false, NICK, HandlerFunc(handleNICK))
 | |
| 		c.Handlers.register(true, false, RPL_NAMREPLY, HandlerFunc(handleNAMES))
 | |
| 
 | |
| 		// Modes.
 | |
| 		c.Handlers.register(true, false, MODE, HandlerFunc(handleMODE))
 | |
| 		c.Handlers.register(true, false, RPL_CHANNELMODEIS, HandlerFunc(handleMODE))
 | |
| 
 | |
| 		// WHO/WHOX responses.
 | |
| 		c.Handlers.register(true, false, RPL_WHOREPLY, HandlerFunc(handleWHO))
 | |
| 		c.Handlers.register(true, false, RPL_WHOSPCRPL, HandlerFunc(handleWHO))
 | |
| 
 | |
| 		// Other misc. useful stuff.
 | |
| 		c.Handlers.register(true, false, TOPIC, HandlerFunc(handleTOPIC))
 | |
| 		c.Handlers.register(true, false, RPL_TOPIC, HandlerFunc(handleTOPIC))
 | |
| 		c.Handlers.register(true, false, RPL_MYINFO, HandlerFunc(handleMYINFO))
 | |
| 		c.Handlers.register(true, false, RPL_ISUPPORT, HandlerFunc(handleISUPPORT))
 | |
| 		c.Handlers.register(true, false, RPL_MOTDSTART, HandlerFunc(handleMOTD))
 | |
| 		c.Handlers.register(true, false, RPL_MOTD, HandlerFunc(handleMOTD))
 | |
| 
 | |
| 		// Keep users lastactive times up to date.
 | |
| 		c.Handlers.register(true, false, PRIVMSG, HandlerFunc(updateLastActive))
 | |
| 		c.Handlers.register(true, false, NOTICE, HandlerFunc(updateLastActive))
 | |
| 		c.Handlers.register(true, false, TOPIC, HandlerFunc(updateLastActive))
 | |
| 		c.Handlers.register(true, false, KICK, HandlerFunc(updateLastActive))
 | |
| 
 | |
| 		// CAP IRCv3-specific tracking and functionality.
 | |
| 		c.Handlers.register(true, false, CAP, HandlerFunc(handleCAP))
 | |
| 		c.Handlers.register(true, false, CAP_CHGHOST, HandlerFunc(handleCHGHOST))
 | |
| 		c.Handlers.register(true, false, CAP_AWAY, HandlerFunc(handleAWAY))
 | |
| 		c.Handlers.register(true, false, CAP_ACCOUNT, HandlerFunc(handleACCOUNT))
 | |
| 		c.Handlers.register(true, false, ALL_EVENTS, HandlerFunc(handleTags))
 | |
| 
 | |
| 		// SASL IRCv3 support.
 | |
| 		c.Handlers.register(true, false, AUTHENTICATE, HandlerFunc(handleSASL))
 | |
| 		c.Handlers.register(true, false, RPL_SASLSUCCESS, HandlerFunc(handleSASL))
 | |
| 		c.Handlers.register(true, false, RPL_NICKLOCKED, HandlerFunc(handleSASLError))
 | |
| 		c.Handlers.register(true, false, ERR_SASLFAIL, HandlerFunc(handleSASLError))
 | |
| 		c.Handlers.register(true, false, ERR_SASLTOOLONG, HandlerFunc(handleSASLError))
 | |
| 		c.Handlers.register(true, false, ERR_SASLABORTED, HandlerFunc(handleSASLError))
 | |
| 		c.Handlers.register(true, false, RPL_SASLMECHS, HandlerFunc(handleSASLError))
 | |
| 	}
 | |
| 
 | |
| 	// Nickname collisions.
 | |
| 	c.Handlers.register(true, false, ERR_NICKNAMEINUSE, HandlerFunc(nickCollisionHandler))
 | |
| 	c.Handlers.register(true, false, ERR_NICKCOLLISION, HandlerFunc(nickCollisionHandler))
 | |
| 	c.Handlers.register(true, false, ERR_UNAVAILRESOURCE, HandlerFunc(nickCollisionHandler))
 | |
| 
 | |
| 	c.Handlers.mu.Unlock()
 | |
| }
 | |
| 
 | |
| // handleConnect is a helper function which lets the client know that enough
 | |
| // time has passed and now they can send commands.
 | |
| //
 | |
| // Should always run in separate thread due to blocking delay.
 | |
| func handleConnect(c *Client, e Event) {
 | |
| 	// This should be the nick that the server gives us. 99% of the time, it's
 | |
| 	// the one we supplied during connection, but some networks will rename
 | |
| 	// users on connect.
 | |
| 	if len(e.Params) > 0 {
 | |
| 		c.state.Lock()
 | |
| 		c.state.nick = e.Params[0]
 | |
| 		c.state.Unlock()
 | |
| 
 | |
| 		c.state.notify(c, UPDATE_GENERAL)
 | |
| 	}
 | |
| 
 | |
| 	time.Sleep(2 * time.Second)
 | |
| 	c.RunHandlers(&Event{Command: CONNECTED, Trailing: c.Server()})
 | |
| }
 | |
| 
 | |
| // nickCollisionHandler helps prevent the client from having conflicting
 | |
| // nicknames with another bot, user, etc.
 | |
| func nickCollisionHandler(c *Client, e Event) {
 | |
| 	if c.Config.HandleNickCollide == nil {
 | |
| 		c.Cmd.Nick(c.GetNick() + "_")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	c.Cmd.Nick(c.Config.HandleNickCollide(c.GetNick()))
 | |
| }
 | |
| 
 | |
| // handlePING helps respond to ping requests from the server.
 | |
| func handlePING(c *Client, e Event) {
 | |
| 	c.Cmd.Pong(e.Trailing)
 | |
| }
 | |
| 
 | |
| func handlePONG(c *Client, e Event) {
 | |
| 	c.conn.mu.Lock()
 | |
| 	c.conn.lastPong = time.Now()
 | |
| 	c.conn.mu.Unlock()
 | |
| }
 | |
| 
 | |
| // handleJOIN ensures that the state has updated users and channels.
 | |
| func handleJOIN(c *Client, e Event) {
 | |
| 	if e.Source == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	var channelName string
 | |
| 	if len(e.Params) > 0 {
 | |
| 		channelName = e.Params[0]
 | |
| 	} else {
 | |
| 		channelName = e.Trailing
 | |
| 	}
 | |
| 
 | |
| 	c.state.Lock()
 | |
| 
 | |
| 	channel := c.state.lookupChannel(channelName)
 | |
| 	if channel == nil {
 | |
| 		if ok := c.state.createChannel(channelName); !ok {
 | |
| 			c.state.Unlock()
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		channel = c.state.lookupChannel(channelName)
 | |
| 	}
 | |
| 
 | |
| 	user := c.state.lookupUser(e.Source.Name)
 | |
| 	if user == nil {
 | |
| 		if ok := c.state.createUser(e.Source.Name); !ok {
 | |
| 			c.state.Unlock()
 | |
| 			return
 | |
| 		}
 | |
| 		user = c.state.lookupUser(e.Source.Name)
 | |
| 	}
 | |
| 
 | |
| 	defer c.state.notify(c, UPDATE_STATE)
 | |
| 
 | |
| 	channel.addUser(user.Nick)
 | |
| 	user.addChannel(channel.Name)
 | |
| 
 | |
| 	// Assume extended-join (ircv3).
 | |
| 	if len(e.Params) == 2 {
 | |
| 		if e.Params[1] != "*" {
 | |
| 			user.Extras.Account = e.Params[1]
 | |
| 		}
 | |
| 
 | |
| 		if len(e.Trailing) > 0 {
 | |
| 			user.Extras.Name = e.Trailing
 | |
| 		}
 | |
| 	}
 | |
| 	c.state.Unlock()
 | |
| 
 | |
| 	if e.Source.Name == c.GetNick() {
 | |
| 		// If it's us, don't just add our user to the list. Run a WHO which
 | |
| 		// will tell us who exactly is in the entire channel.
 | |
| 		c.Send(&Event{Command: WHO, Params: []string{channelName, "%tacuhnr,1"}})
 | |
| 
 | |
| 		// Also send a MODE to obtain the list of channel modes.
 | |
| 		c.Send(&Event{Command: MODE, Params: []string{channelName}})
 | |
| 
 | |
| 		// Update our ident and host too, in state -- since there is no
 | |
| 		// cleaner method to do this.
 | |
| 		c.state.Lock()
 | |
| 		c.state.ident = e.Source.Ident
 | |
| 		c.state.host = e.Source.Host
 | |
| 		c.state.Unlock()
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Only WHO the user, which is more efficient.
 | |
| 	c.Send(&Event{Command: WHO, Params: []string{e.Source.Name, "%tacuhnr,1"}})
 | |
| }
 | |
| 
 | |
| // handlePART ensures that the state is clean of old user and channel entries.
 | |
| func handlePART(c *Client, e Event) {
 | |
| 	if e.Source == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	var channel string
 | |
| 	if len(e.Params) > 0 {
 | |
| 		channel = e.Params[0]
 | |
| 	} else {
 | |
| 		channel = e.Trailing
 | |
| 	}
 | |
| 
 | |
| 	if channel == "" {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	defer c.state.notify(c, UPDATE_STATE)
 | |
| 
 | |
| 	if e.Source.Name == c.GetNick() {
 | |
| 		c.state.Lock()
 | |
| 		c.state.deleteChannel(channel)
 | |
| 		c.state.Unlock()
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	c.state.Lock()
 | |
| 	c.state.deleteUser(channel, e.Source.Name)
 | |
| 	c.state.Unlock()
 | |
| }
 | |
| 
 | |
| // handleTOPIC handles incoming TOPIC events and keeps channel tracking info
 | |
| // updated with the latest channel topic.
 | |
| func handleTOPIC(c *Client, e Event) {
 | |
| 	var name string
 | |
| 	switch len(e.Params) {
 | |
| 	case 0:
 | |
| 		return
 | |
| 	case 1:
 | |
| 		name = e.Params[0]
 | |
| 	default:
 | |
| 		name = e.Params[len(e.Params)-1]
 | |
| 	}
 | |
| 
 | |
| 	c.state.Lock()
 | |
| 	channel := c.state.lookupChannel(name)
 | |
| 	if channel == nil {
 | |
| 		c.state.Unlock()
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	channel.Topic = e.Trailing
 | |
| 	c.state.Unlock()
 | |
| 	c.state.notify(c, UPDATE_STATE)
 | |
| }
 | |
| 
 | |
| // handlWHO updates our internal tracking of users/channels with WHO/WHOX
 | |
| // information.
 | |
| func handleWHO(c *Client, e Event) {
 | |
| 	var ident, host, nick, account, realname string
 | |
| 
 | |
| 	// Assume WHOX related.
 | |
| 	if e.Command == RPL_WHOSPCRPL {
 | |
| 		if len(e.Params) != 7 {
 | |
| 			// Assume there was some form of error or invalid WHOX response.
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		if e.Params[1] != "1" {
 | |
| 			// We should always be sending 1, and we should receive 1. If this
 | |
| 			// is anything but, then we didn't send the request and we can
 | |
| 			// ignore it.
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		ident, host, nick, account = e.Params[3], e.Params[4], e.Params[5], e.Params[6]
 | |
| 		realname = e.Trailing
 | |
| 	} else {
 | |
| 		// Assume RPL_WHOREPLY.
 | |
| 		ident, host, nick = e.Params[2], e.Params[3], e.Params[5]
 | |
| 		if len(e.Trailing) > 2 {
 | |
| 			realname = e.Trailing[2:]
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	c.state.Lock()
 | |
| 	user := c.state.lookupUser(nick)
 | |
| 	if user == nil {
 | |
| 		c.state.Unlock()
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	user.Host = host
 | |
| 	user.Ident = ident
 | |
| 	user.Extras.Name = realname
 | |
| 
 | |
| 	if account != "0" {
 | |
| 		user.Extras.Account = account
 | |
| 	}
 | |
| 
 | |
| 	c.state.Unlock()
 | |
| 	c.state.notify(c, UPDATE_STATE)
 | |
| }
 | |
| 
 | |
| // handleKICK ensures that users are cleaned up after being kicked from the
 | |
| // channel
 | |
| func handleKICK(c *Client, e Event) {
 | |
| 	if len(e.Params) < 2 {
 | |
| 		// Needs at least channel and user.
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	defer c.state.notify(c, UPDATE_STATE)
 | |
| 
 | |
| 	if e.Params[1] == c.GetNick() {
 | |
| 		c.state.Lock()
 | |
| 		c.state.deleteChannel(e.Params[0])
 | |
| 		c.state.Unlock()
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Assume it's just another user.
 | |
| 	c.state.Lock()
 | |
| 	c.state.deleteUser(e.Params[0], e.Params[1])
 | |
| 	c.state.Unlock()
 | |
| }
 | |
| 
 | |
| // handleNICK ensures that users are renamed in state, or the client name is
 | |
| // up to date.
 | |
| func handleNICK(c *Client, e Event) {
 | |
| 	if e.Source == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	c.state.Lock()
 | |
| 	// renameUser updates the LastActive time automatically.
 | |
| 	if len(e.Params) == 1 {
 | |
| 		c.state.renameUser(e.Source.Name, e.Params[0])
 | |
| 	} else if len(e.Trailing) > 0 {
 | |
| 		c.state.renameUser(e.Source.Name, e.Trailing)
 | |
| 	}
 | |
| 	c.state.Unlock()
 | |
| 	c.state.notify(c, UPDATE_STATE)
 | |
| }
 | |
| 
 | |
| // handleQUIT handles users that are quitting from the network.
 | |
| func handleQUIT(c *Client, e Event) {
 | |
| 	if e.Source == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if e.Source.Name == c.GetNick() {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	c.state.Lock()
 | |
| 	c.state.deleteUser("", e.Source.Name)
 | |
| 	c.state.Unlock()
 | |
| 	c.state.notify(c, UPDATE_STATE)
 | |
| }
 | |
| 
 | |
| // handleMYINFO handles incoming MYINFO events -- these are commonly used
 | |
| // to tell us what the server name is, what version of software is being used
 | |
| // as well as what channel and user modes are being used on the server.
 | |
| func handleMYINFO(c *Client, e Event) {
 | |
| 	// Malformed or odd output. As this can differ strongly between networks,
 | |
| 	// just skip it.
 | |
| 	if len(e.Params) < 3 {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	c.state.Lock()
 | |
| 	c.state.serverOptions["SERVER"] = e.Params[1]
 | |
| 	c.state.serverOptions["VERSION"] = e.Params[2]
 | |
| 	c.state.Unlock()
 | |
| 	c.state.notify(c, UPDATE_GENERAL)
 | |
| }
 | |
| 
 | |
| // handleISUPPORT handles incoming RPL_ISUPPORT (also known as RPL_PROTOCTL)
 | |
| // events. These commonly contain the server capabilities and limitations.
 | |
| // For example, things like max channel name length, or nickname length.
 | |
| func handleISUPPORT(c *Client, e Event) {
 | |
| 	// Must be a ISUPPORT-based message. 005 is also used for server bounce
 | |
| 	// related things, so this handler may be triggered during other
 | |
| 	// situations.
 | |
| 
 | |
| 	// Also known as RPL_PROTOCTL.
 | |
| 	if !strings.HasSuffix(e.Trailing, "this server") {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Must have at least one configuration.
 | |
| 	if len(e.Params) < 2 {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	c.state.Lock()
 | |
| 	// Skip the first parameter, as it's our nickname.
 | |
| 	for i := 1; i < len(e.Params); i++ {
 | |
| 		j := strings.IndexByte(e.Params[i], '=')
 | |
| 
 | |
| 		if j < 1 || (j+1) == len(e.Params[i]) {
 | |
| 			c.state.serverOptions[e.Params[i]] = ""
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		name := e.Params[i][0:j]
 | |
| 		val := e.Params[i][j+1:]
 | |
| 		c.state.serverOptions[name] = val
 | |
| 	}
 | |
| 	c.state.Unlock()
 | |
| 	c.state.notify(c, UPDATE_GENERAL)
 | |
| }
 | |
| 
 | |
| // handleMOTD handles incoming MOTD messages and buffers them up for use with
 | |
| // Client.ServerMOTD().
 | |
| func handleMOTD(c *Client, e Event) {
 | |
| 	c.state.Lock()
 | |
| 
 | |
| 	defer c.state.notify(c, UPDATE_GENERAL)
 | |
| 
 | |
| 	// Beginning of the MOTD.
 | |
| 	if e.Command == RPL_MOTDSTART {
 | |
| 		c.state.motd = ""
 | |
| 
 | |
| 		c.state.Unlock()
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Otherwise, assume we're getting sent the MOTD line-by-line.
 | |
| 	if len(c.state.motd) != 0 {
 | |
| 		e.Trailing = "\n" + e.Trailing
 | |
| 	}
 | |
| 
 | |
| 	c.state.motd += e.Trailing
 | |
| 	c.state.Unlock()
 | |
| }
 | |
| 
 | |
| // handleNAMES handles incoming NAMES queries, of which lists all users in
 | |
| // a given channel. Optionally also obtains ident/host values, as well as
 | |
| // permissions for each user, depending on what capabilities are enabled.
 | |
| func handleNAMES(c *Client, e Event) {
 | |
| 	if len(e.Params) < 1 {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	channel := c.state.lookupChannel(e.Params[len(e.Params)-1])
 | |
| 	if channel == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	parts := strings.Split(e.Trailing, " ")
 | |
| 
 | |
| 	var host, ident, modes, nick string
 | |
| 	var ok bool
 | |
| 
 | |
| 	c.state.Lock()
 | |
| 	for i := 0; i < len(parts); i++ {
 | |
| 		modes, nick, ok = parseUserPrefix(parts[i])
 | |
| 		if !ok {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// If userhost-in-names.
 | |
| 		if strings.Contains(nick, "@") {
 | |
| 			s := ParseSource(nick)
 | |
| 			if s == nil {
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			host = s.Host
 | |
| 			nick = s.Name
 | |
| 			ident = s.Ident
 | |
| 		}
 | |
| 
 | |
| 		if !IsValidNick(nick) {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		c.state.createUser(nick)
 | |
| 		user := c.state.lookupUser(nick)
 | |
| 		if user == nil {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		user.addChannel(channel.Name)
 | |
| 		channel.addUser(nick)
 | |
| 
 | |
| 		// Add necessary userhost-in-names data into the user.
 | |
| 		if host != "" {
 | |
| 			user.Host = host
 | |
| 		}
 | |
| 		if ident != "" {
 | |
| 			user.Ident = ident
 | |
| 		}
 | |
| 
 | |
| 		// Don't append modes, overwrite them.
 | |
| 		perms, _ := user.Perms.Lookup(channel.Name)
 | |
| 		perms.set(modes, false)
 | |
| 		user.Perms.set(channel.Name, perms)
 | |
| 	}
 | |
| 	c.state.Unlock()
 | |
| 	c.state.notify(c, UPDATE_STATE)
 | |
| }
 | |
| 
 | |
| // updateLastActive is a wrapper for any event which the source author
 | |
| // should have it's LastActive time updated. This is useful for things like
 | |
| // a KICK where we know they are active, as they just kicked another user,
 | |
| // even though they may not be talking.
 | |
| func updateLastActive(c *Client, e Event) {
 | |
| 	if e.Source == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	c.state.Lock()
 | |
| 
 | |
| 	// Update the users last active time, if they exist.
 | |
| 	user := c.state.lookupUser(e.Source.Name)
 | |
| 	if user == nil {
 | |
| 		c.state.Unlock()
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	user.LastActive = time.Now()
 | |
| 	c.state.Unlock()
 | |
| }
 | 
