summaryrefslogblamecommitdiff
path: root/irc/session.go
blob: 875ed1140372ff7b3acbccbce5edd72631ba52bf (plain) (tree)
1
2
3
4
5
6
7
8
9


           



                         
              
                 
                 
              
                 
                      

                                


                           
                    








                                                         



                                     






                                                                          
                                                        










                                                                    
                                                                              
                                                










                               
 





                                              

 

                                                                               






                           
                            
                  


                                                                                 

 
                               
                     




                                                                                          
 
                                                                     

 
                                                         



                           

                           


                     

                                   
                         

                                                                             
 
                     
                                             



                     
                     
                         
 

                                         

                            
                                         
                               




                            
                          
                          
 





                                                                                                          

                                                                                                 
                                                                                                                           

                                                                                         
 
                             
                             

 
                                                                    
                      






                                                               
                                              
                                             
                                                     










                                                           
                                                       
                                                        

         
                                               


                                                             

                                                             










                                                                
 



                                   
                

 

                           

                      
                       
                         
                    

 
                                                                         
                








                                                         



                                  
                                     

                                   

 



                                          
                                               
                                                       

 
                                               
                              

 
                                                 







                                                  

                                                                            
                                               
                                                 
                          




                                                                 



                                                                     

                                  
                 

                                                           


                                                     





                                             
         
                                 


                    
                                                                 
                                                   







                                                           

                 
                         


                  



                                               

                                                            
                                                                     














                                                                                   
                                               


                                
         
              

 
                                       
                                

 
                                             

                                                 




                                                         

 
                                                
                                                    

 

                                                      

 

                                           

 

                                           

 



                                                          



                                   








                                                                                                  
                                                                    




                                                                
                                            

 











                                                          


















                                                            
                                                   








                                                

                                                     
                                      
                                                             
         

                                        

 

                                             

                      
                                     
                         

                                                                                                    

                      








                                                                 
                                                                          

 

                                             

                      















                                                                                                

                                                                        
 
                                          

                                                            



                                                               

                                                                                   


         



















                                                                   





                            

 
                                          
                   

                                                                                                   

 




                                                               
         

                
 

                                                    

                      
 




                                              
 

                                                  


                                             


                                                     

 





                                               




                                               
 






                                                                         






                                                                    
 



                                          



                                                    







                                                                   
                                                             



                                                


         
                                                                  
                            
                              





                                                                   
                            


                                           
                
                                              
         
                       

 
                                                                
                                            









                                                                                         








                                                                                           
                                                        
                                                                       


                                               




                                                                         
                         
                                       

                 

                                                    
 
                                                                                      
                            





















                                                                           



                                                                                                      


                                           




                                                                                                            
                        



                                                                
                                            
                                   


                                                                 
                                 
                                     
                 
                         


                                                             
                                                                   





                                                            
                                             
                                                      







                                                                                                  


                                       
                                         
                                                        

                                       
                                         
                                     
                 





                                                 
                   





                                                                                
                           
                                                           




                                                                          
 



                                                                                            
                                                                    

                                                                                     

                                         
                         

                                     
                           
                                                           
                                                                 



                                                                                
                                                
                                 

                                                                         
                           
                                                           
                                                               
                                                             


                         








                                                                 







                                                         
                                                    


                                               
                                                        
                                                       
                                                            
                         



                                                                              
                                              
                         

                                                              
                                                                                
                         
                                                       

                                                         
                                                
                                                         
                              
                 
                    








                                                                 







                                                         
                                                    


                                               




                                                               

                                                        
                                      
                         
                                                              


                                                         
                                                                 

                                                             
                                                        
                                                                 
                                      
                         
                 
                    




                                                                        







                                                         



                                               




                                                               

                                                        
                                      





                                                                 
                                                     
                                                      
                                                        
                                                                 
                                      

                         
                    



                                                    






                                                      
                                                    
 
                                                 
                                             
                                             
                                                              



                                                                           
                                                                         
                                 
                         

                                                      
                                                   
                                                          
                              
                 















































                                                                          
                         





                                                                                   
 
                                                       
 
                                                                                     
                                                                   
 
                                                                  
                                                                                       
                                 
                                                                            
                         

                                                 
                 
                           






                                                                      


                                                                      
                                            
                                                
                                                 
                         
                                                                                                               

                                                   
                                      
                 
                      






                                                                              
                                                       
                                       

                                                 
                             









                                                                                             
                                                       
                                                          


                                                     
                        






                                                                      




                                                       








                                                                         







                                                         

                                               
                                                       
                                       
                                                      
                                                     
                                                 
                                                

                                                 
                                                         
                              
                 
                    

                                                                      

                                       
                                                         
 







                                                         

                                               
                                                       
                                                                                                                       




























                                                                                                          

                                                

                                                         
                              
                 

























                                                                             









                                                     
                                 








                                                                



                                                     
                                             

                                                    
 
                                             
                      



                                       










                                                                
 
                                            



                                           
                                                     

                                      
                                                                  
                                              
                                                                
                         
                 
                     



                                                            


                                                                
 
                                          
                           
 













                                                                                          


                                                                                                         


                                                             









                                                                     


                                                         
                         
                 
                    








                                                              







                                                            
                                                    
                               
                                               
 
                                                          
                                                      

                                                       

                             
                 
 
                                            
                                        
                                            

                                                            
                              
                        
                                             
                                                 
                                                            
                                                            
                              
                 
                        


















                                                                            







                                                                









                                                         
                    





                                                                 

                         




                                                                   
                 







                                               
                 
 
                                  

                                           
                                                                    
                      

                                                                                         

                                                                                          
                
                                  


                                                                     





                                                                             



                                                                                                     

                                                                     
                                                      
                                                                            
                              
                 
         
                       

 










                                                                             
                                                                   
                                                                   
                                     
                                 

                                         
 




                                                                                 
                                     


                                              
         

                      

 
                                           



                                             




                                                   
                               

 
























                                                                 




                                                          
               
                            

                                       






                                                          





                                                                                 

                                           




                                                                




                                                           




                                                           



                                                    
                                            






                                                                

                                 


                                                                

                                     

                 
 

                                     


                         

                                                               
                                                 
                
                                                 
                                                              

         
package irc

import (
	"bytes"
	"encoding/base64"
	"errors"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"time"
	"unicode"
	"unicode/utf8"

	"golang.org/x/time/rate"
)

type SASLClient interface {
	Early() bool
	Handshake() (mech string)
	Respond(challenge string) (res string, err error)
}

type SASLPlain struct {
	Username string
	Password string
}

func (auth *SASLPlain) Early() bool {
	return true
}

func (auth *SASLPlain) Handshake() (mech string) {
	mech = "PLAIN"
	return
}

func (auth *SASLPlain) Respond(challenge string) (res string, err error) {
	if challenge != "+" {
		err = errors.New("unexpected challenge")
		return
	}

	user := []byte(auth.Username)
	pass := []byte(auth.Password)
	payload := bytes.Join([][]byte{user, user, pass}, []byte{0})
	res = base64.StdEncoding.EncodeToString(payload)

	return
}

// SupportedCapabilities is the set of capabilities supported by this library.
var SupportedCapabilities = map[string]struct{}{
	"away-notify":      {},
	"batch":            {},
	"cap-notify":       {},
	"echo-message":     {},
	"extended-monitor": {},
	"invite-notify":    {},
	"message-tags":     {},
	"multi-prefix":     {},
	"server-time":      {},
	"sasl":             {},
	"setname":          {},

	"draft/chathistory":               {},
	"draft/event-playback":            {},
	"draft/read-marker":               {},
	"soju.im/bouncer-networks-notify": {},
	"soju.im/bouncer-networks":        {},
	"soju.im/search":                  {},
}

// Values taken by the "@+typing=" client tag.  TypingUnspec means the value or
// tag is absent.
const (
	TypingUnspec = iota
	TypingActive
	TypingPaused
	TypingDone
)

// User is a known IRC user.
type User struct {
	Name         *Prefix // the nick, user and hostname of the user if known.
	Away         bool    // whether the user is away or not
	Disconnected bool    // can only be true for monitored users.
}

// Channel is a joined channel.
type Channel struct {
	Name      string           // the name of the channel.
	Members   map[*User]string // the set of members associated with their membership.
	Topic     string           // the topic of the channel, or "" if absent.
	TopicWho  *Prefix          // the name of the last user who set the topic.
	TopicTime time.Time        // the last time the topic has been changed.

	complete bool // whether this structure is fully initialized.
}

// SessionParams defines how to connect to an IRC server.
type SessionParams struct {
	Nickname string
	Username string
	RealName string
	NetID    string
	Auth     SASLClient
}

type Session struct {
	out          chan<- Message
	closed       bool
	registered   bool
	typings      *Typings               // incoming typing notifications.
	typingStamps map[string]typingStamp // user typing instants.

	nick   string
	nickCf string // casemapped nickname.
	user   string
	real   string
	acct   string
	host   string
	netID  string
	auth   SASLClient

	availableCaps map[string]string
	enabledCaps   map[string]struct{}

	// ISUPPORT features
	casemap       func(string) string
	chanmodes     [4]string
	chantypes     string
	linelen       int
	historyLimit  int
	prefixSymbols string
	prefixModes   string
	monitor       bool
	whox          bool

	users          map[string]*User        // known users.
	channels       map[string]Channel      // joined channels.
	chBatches      map[string]HistoryEvent // channel history batches being processed.
	chReqs         map[string]struct{}     // set of targets for which history is currently requested.
	targetsBatchID string                  // ID of the channel history targets batch being processed.
	targetsBatch   HistoryTargetsEvent     // channel history targets batch being processed.
	searchBatchID  string                  // ID of the search targets batch being processed.
	searchBatch    SearchEvent             // search batch being processed.
	monitors       map[string]struct{}     // set of users we want to monitor (and keep even if they are disconnected).

	pendingChannels map[string]time.Time // set of join requests stamps for channels.

	receivedISupport bool
	receivedUserMode bool
}

func NewSession(out chan<- Message, params SessionParams) *Session {
	s := &Session{
		out:             out,
		typings:         NewTypings(),
		typingStamps:    map[string]typingStamp{},
		nick:            params.Nickname,
		nickCf:          CasemapASCII(params.Nickname),
		user:            params.Username,
		real:            params.RealName,
		netID:           params.NetID,
		auth:            params.Auth,
		availableCaps:   map[string]string{},
		enabledCaps:     map[string]struct{}{},
		casemap:         CasemapRFC1459,
		chantypes:       "#&",
		linelen:         512,
		historyLimit:    100,
		prefixSymbols:   "@+",
		prefixModes:     "ov",
		users:           map[string]*User{},
		channels:        map[string]Channel{},
		chBatches:       map[string]HistoryEvent{},
		chReqs:          map[string]struct{}{},
		monitors:        map[string]struct{}{},
		pendingChannels: map[string]time.Time{},
	}

	s.out <- NewMessage("CAP", "LS", "302")
	for capability := range SupportedCapabilities {
		s.out <- NewMessage("CAP", "REQ", capability)
	}
	s.out <- NewMessage("NICK", s.nick)
	s.out <- NewMessage("USER", s.user, "0", "*", s.real)
	if s.auth != nil && s.auth.Early() {
		h := s.auth.Handshake()
		s.out <- NewMessage("AUTHENTICATE", h)
		res, err := s.auth.Respond("+")
		if err != nil {
			s.out <- NewMessage("AUTHENTICATE", "*")
		} else {
			s.out <- NewMessage("AUTHENTICATE", res)
		}
		s.auth = nil
	}

	if s.auth == nil {
		s.endRegistration()
	}

	return s
}

func (s *Session) Close() {
	if s.closed {
		return
	}
	s.closed = true
	s.typings.Close()
	close(s.out)
}

// HasCapability reports whether the given capability has been negotiated
// successfully.
func (s *Session) HasCapability(capability string) bool {
	_, ok := s.enabledCaps[capability]
	return ok
}

func (s *Session) Nick() string {
	return s.nick
}

func (s *Session) NetID() string {
	return s.netID
}

// NickCf is our casemapped nickname.
func (s *Session) NickCf() string {
	return s.nickCf
}

func (s *Session) IsMe(nick string) bool {
	return s.nickCf == s.casemap(nick)
}

func (s *Session) IsChannel(name string) bool {
	return strings.IndexAny(name, s.chantypes) == 0
}

func (s *Session) Casemap(name string) string {
	return s.casemap(name)
}

// Users returns the list of all known nicknames.
func (s *Session) Users() []string {
	users := make([]string, 0, len(s.users))
	for _, u := range s.users {
		users = append(users, u.Name.Name)
	}
	return users
}

// Names returns the list of users in the given target, or nil if the target
// is not a known channel or nick in the session.
// The list is sorted according to member name.
func (s *Session) Names(target string) []Member {
	var names []Member
	if s.IsChannel(target) {
		if c, ok := s.channels[s.Casemap(target)]; ok {
			names = make([]Member, 0, len(c.Members))
			for u, pl := range c.Members {
				names = append(names, Member{
					PowerLevel:   pl,
					Name:         u.Name.Copy(),
					Away:         u.Away,
					Disconnected: u.Disconnected,
				})
			}
		}
	} else if u, ok := s.users[s.Casemap(target)]; ok {
		names = append(names, Member{
			Name:         u.Name.Copy(),
			Away:         u.Away,
			Disconnected: u.Disconnected,
		})
		names = append(names, Member{
			Name: &Prefix{
				Name: s.nick,
			},
		})
	}
	sort.Sort(members(names))
	return names
}

// Typings returns the list of nickname who are currently typing.
func (s *Session) Typings(target string) []string {
	targetCf := s.casemap(target)
	res := s.typings.List(targetCf)
	for i := 0; i < len(res); i++ {
		if s.IsMe(res[i]) {
			res = append(res[:i], res[i+1:]...)
			i--
		} else if u, ok := s.users[res[i]]; ok {
			res[i] = u.Name.Name
		}
	}
	sort.Strings(res)
	return res
}

func (s *Session) TypingStops() <-chan Typing {
	return s.typings.Stops()
}

func (s *Session) ChannelsSharedWith(name string) []string {
	var user *User
	if u, ok := s.users[s.Casemap(name)]; ok && !u.Disconnected {
		user = u
	} else {
		return nil
	}
	var channels []string
	for _, c := range s.channels {
		if _, ok := c.Members[user]; ok {
			channels = append(channels, c.Name)
		}
	}
	return channels
}

func (s *Session) Topic(channel string) (topic string, who *Prefix, at time.Time) {
	channelCf := s.Casemap(channel)
	if c, ok := s.channels[channelCf]; ok {
		topic = c.Topic
		who = c.TopicWho
		at = c.TopicTime
	}
	return
}

func (s *Session) SendRaw(raw string) {
	s.out <- NewMessage(raw)
}

func (s *Session) Join(channel, key string) {
	channelCf := s.Casemap(channel)
	s.pendingChannels[channelCf] = time.Now()
	if key == "" {
		s.out <- NewMessage("JOIN", channel)
	} else {
		s.out <- NewMessage("JOIN", channel, key)
	}
}

func (s *Session) Part(channel, reason string) {
	s.out <- NewMessage("PART", channel, reason)
}

func (s *Session) ChangeTopic(channel, topic string) {
	s.out <- NewMessage("TOPIC", channel, topic)
}

func (s *Session) Quit(reason string) {
	s.out <- NewMessage("QUIT", reason)
}

func (s *Session) ChangeNick(nick string) {
	s.out <- NewMessage("NICK", nick)
}

func (s *Session) Oper(username string, password string) {
	s.out <- NewMessage("OPER", username, password)
}

func (s *Session) MOTD() {
	s.out <- NewMessage("MOTD")
}

func (s *Session) Who(target string) {
	if s.whox {
		// only request what we need, to optimize server who cache hits and reduce traffic
		s.out <- NewMessage("WHO", target, "%uhnf")
	} else {
		s.out <- NewMessage("WHO", target)
	}
}

func (s *Session) ChangeMode(channel, flags string, args []string) {
	if flags != "" {
		args = append([]string{channel, flags}, args...)
	} else {
		args = append([]string{channel}, args...)
	}
	s.out <- NewMessage("MODE", args...)
}

func (s *Session) Search(target, text string) {
	if _, ok := s.enabledCaps["soju.im/search"]; !ok {
		return
	}
	attrs := make(map[string]string)
	attrs["text"] = text
	if target != "" {
		attrs["in"] = target
	}
	s.out <- NewMessage("SEARCH", formatTags(attrs))
}

func splitChunks(s string, chunkLen int) (chunks []string) {
	if chunkLen <= 0 {
		return []string{s}
	}
	for chunkLen < len(s) {
		i := chunkLen
		min := chunkLen - utf8.UTFMax
		for min <= i && !utf8.RuneStart(s[i]) {
			i--
		}
		chunks = append(chunks, s[:i])
		s = s[i:]
	}
	if len(s) != 0 {
		chunks = append(chunks, s)
	}
	return
}

func (s *Session) PrivMsg(target, content string) {
	hostLen := len(s.host)
	if hostLen == 0 {
		hostLen = len("255.255.255.255")
	}
	maxMessageLen := s.linelen -
		len(":!@ PRIVMSG  :\r\n") -
		len(s.nick) -
		len(s.user) -
		hostLen -
		len(target)
	chunks := splitChunks(content, maxMessageLen)
	for _, chunk := range chunks {
		s.out <- NewMessage("PRIVMSG", target, chunk)
	}
	targetCf := s.Casemap(target)
	delete(s.typingStamps, targetCf)
}

func (s *Session) Typing(target string) {
	if !s.HasCapability("message-tags") {
		return
	}
	targetCf := s.casemap(target)
	now := time.Now()
	t, ok := s.typingStamps[targetCf]
	if ok && ((t.Type == TypingActive && now.Sub(t.Last).Seconds() < 3.0) || !t.Limit.Allow()) {
		return
	}
	if !ok {
		t.Limit = rate.NewLimiter(rate.Limit(1.0/3.0), 5)
		t.Limit.Reserve() // will always be OK
	}
	s.typingStamps[targetCf] = typingStamp{
		Last:  now,
		Type:  TypingActive,
		Limit: t.Limit,
	}
	s.out <- NewMessage("TAGMSG", target).WithTag("+typing", "active")
}

func (s *Session) TypingStop(target string) {
	if !s.HasCapability("message-tags") {
		return
	}
	targetCf := s.casemap(target)
	now := time.Now()
	t, ok := s.typingStamps[targetCf]
	if ok && (t.Type == TypingDone || !t.Limit.Allow()) {
		// don't send a +typing=done again if the last typing we sent was a +typing=done
		return
	}
	if !ok {
		t.Limit = rate.NewLimiter(rate.Limit(1), 5)
		t.Limit.Reserve() // will always be OK
	}
	s.typingStamps[targetCf] = typingStamp{
		Last:  now,
		Type:  TypingDone,
		Limit: t.Limit,
	}
	s.out <- NewMessage("TAGMSG", target).WithTag("+typing", "done")
}

func (s *Session) ReadGet(target string) {
	if _, ok := s.enabledCaps["draft/read-marker"]; ok {
		s.out <- NewMessage("MARKREAD", target)
	}
}

func (s *Session) ReadSet(target string, timestamp time.Time) {
	if _, ok := s.enabledCaps["draft/read-marker"]; ok {
		s.out <- NewMessage("MARKREAD", target, formatTimestamp(timestamp))
	}
}

func (s *Session) MonitorAdd(target string) {
	targetCf := s.casemap(target)
	if _, ok := s.monitors[targetCf]; !ok {
		s.monitors[targetCf] = struct{}{}
		if s.monitor {
			s.out <- NewMessage("MONITOR", "+", target)
		}
	}
}

func (s *Session) MonitorRemove(target string) {
	targetCf := s.casemap(target)
	if _, ok := s.monitors[targetCf]; ok {
		delete(s.monitors, targetCf)
		if s.monitor {
			s.out <- NewMessage("MONITOR", "-", target)
		}
	}
}

type HistoryRequest struct {
	s       *Session
	target  string
	command string
	bounds  []string
	limit   int
}

func formatTimestamp(t time.Time) string {
	t = t.UTC()
	return fmt.Sprintf("timestamp=%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
		t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()/1e6)
}

func (r *HistoryRequest) WithLimit(limit int) *HistoryRequest {
	if limit < r.s.historyLimit {
		r.limit = limit
	} else {
		r.limit = r.s.historyLimit
	}
	return r
}

func (r *HistoryRequest) doRequest() {
	if !r.s.HasCapability("draft/chathistory") {
		return
	}

	targetCf := r.s.casemap(r.target)
	if _, ok := r.s.chReqs[targetCf]; ok {
		return
	}
	r.s.chReqs[targetCf] = struct{}{}

	args := make([]string, 0, len(r.bounds)+3)
	args = append(args, r.command)
	if r.target != "" {
		args = append(args, r.target)
	}
	args = append(args, r.bounds...)
	args = append(args, strconv.Itoa(r.limit))
	r.s.out <- NewMessage("CHATHISTORY", args...)
}

func (r *HistoryRequest) After(t time.Time) {
	r.command = "AFTER"
	r.bounds = []string{formatTimestamp(t)}
	r.doRequest()
}

func (r *HistoryRequest) Before(t time.Time) {
	r.command = "BEFORE"
	r.bounds = []string{formatTimestamp(t)}
	r.doRequest()
}

func (r *HistoryRequest) Targets(start time.Time, end time.Time) {
	r.command = "TARGETS"
	r.bounds = []string{formatTimestamp(start), formatTimestamp(end)}
	r.target = ""
	r.doRequest()
}

func (s *Session) NewHistoryRequest(target string) *HistoryRequest {
	return &HistoryRequest{
		s:      s,
		target: target,
		limit:  s.historyLimit,
	}
}

func (s *Session) Whois(nick string) {
	s.out <- NewMessage("WHOIS", nick)
}

func (s *Session) Invite(nick, channel string) {
	s.out <- NewMessage("INVITE", nick, channel)
}

func (s *Session) Kick(nick, channel, comment string) {
	if comment == "" {
		s.out <- NewMessage("KICK", channel, nick)
	} else {
		s.out <- NewMessage("KICK", channel, nick, comment)
	}
}

func (s *Session) HandleMessage(msg Message) (Event, error) {
	if s.registered {
		return s.handleRegistered(msg)
	} else {
		return s.handleUnregistered(msg)
	}
}

func (s *Session) handleUnregistered(msg Message) (Event, error) {
	switch msg.Command {
	case errNicknameinuse:
		var nick string
		if err := msg.ParseParams(nil, &nick); err != nil {
			return nil, err
		}

		s.out <- NewMessage("NICK", nick+"_")
	case rplSaslsuccess:
		if s.auth != nil {
			s.endRegistration()
		}
	default:
		return s.handleRegistered(msg)
	}
	return nil, nil
}

func (s *Session) handleRegistered(msg Message) (Event, error) {
	if id, ok := msg.Tags["batch"]; ok {
		if id == s.targetsBatchID {
			var target, timestamp string
			if err := msg.ParseParams(nil, &target, &timestamp); err != nil {
				return nil, err
			}
			t, ok := parseTimestamp(timestamp)
			if !ok {
				return nil, nil
			}
			s.targetsBatch.Targets[target] = t
		} else if id == s.searchBatchID {
			ev, err := s.handleMessageRegistered(msg, true)
			if err != nil {
				return nil, err
			}
			if ev, ok := ev.(MessageEvent); ok {
				s.searchBatch.Messages = append(s.searchBatch.Messages, ev)
				return nil, nil
			}
		} else if b, ok := s.chBatches[id]; ok {
			ev, err := s.handleMessageRegistered(msg, true)
			if err != nil {
				return nil, err
			}
			if ev != nil {
				s.chBatches[id] = HistoryEvent{
					Target:   b.Target,
					Messages: append(b.Messages, ev),
				}
			}
			return nil, nil
		}
	}
	return s.handleMessageRegistered(msg, false)
}

func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, error) {
	switch msg.Command {
	case "AUTHENTICATE":
		if s.auth == nil {
			break
		}

		var payload string
		if err := msg.ParseParams(&payload); err != nil {
			return nil, err
		}

		res, err := s.auth.Respond(payload)
		if err != nil {
			s.out <- NewMessage("AUTHENTICATE", "*")
		} else {
			s.out <- NewMessage("AUTHENTICATE", res)
		}
	case rplLoggedin:
		var nuh string
		if err := msg.ParseParams(nil, &nuh, &s.acct); err != nil {
			return nil, err
		}

		prefix := ParsePrefix(nuh)
		s.user = prefix.User
		s.host = prefix.Host
	case errNicklocked, errSaslfail, errSasltoolong, errSaslaborted, errSaslalready, rplSaslmechs:
		if s.auth != nil {
			s.endRegistration()
		}
		return ErrorEvent{
			Severity: SeverityFail,
			Code:     msg.Command,
			Message:  fmt.Sprintf("Registration failed: %s", strings.Join(msg.Params[1:], " ")),
		}, nil
	case rplWelcome:
		if err := msg.ParseParams(&s.nick); err != nil {
			return nil, err
		}

		s.nickCf = s.Casemap(s.nick)
		s.registered = true
		s.users[s.nickCf] = &User{Name: &Prefix{
			Name: s.nick, User: s.user, Host: s.host,
		}}
		if s.host == "" {
			s.Who(s.nick)
		}
	case rplIsupport:
		if len(msg.Params) < 3 {
			return nil, msg.errNotEnoughParams(3)
		}
		s.updateFeatures(msg.Params[1 : len(msg.Params)-1])
		if !s.receivedISupport {
			// notify only on first RPL_ISUPPORT
			s.receivedISupport = true
			return RegisteredEvent{}, nil
		}
		return nil, nil
	case rplWhoreply, rplWhospecialreply:
		var nick, host, flags, username string
		var err error
		if msg.Command == rplWhoreply {
			err = msg.ParseParams(nil, nil, &username, &host, nil, &nick, &flags, nil)
		} else {
			// we always request WHOX with %uhnf
			err = msg.ParseParams(nil, &username, &host, &nick, &flags)
		}
		if err != nil {
			return nil, err
		}

		nickCf := s.Casemap(nick)
		away := strings.ContainsRune(flags, 'G')

		if s.nickCf == nickCf {
			s.user = username
			s.host = host
		}

		if u, ok := s.users[nickCf]; ok {
			u.Away = away
		}
	case rplEndofwho:
		// do nothing
	case "CAP":
		var subcommand, caps string
		if err := msg.ParseParams(nil, &subcommand, &caps); err != nil {
			return nil, err
		}

		switch subcommand {
		case "ACK":
			for _, c := range ParseCaps(caps) {
				if c.Enable {
					s.enabledCaps[c.Name] = struct{}{}
				} else {
					delete(s.enabledCaps, c.Name)
				}

				if s.auth != nil && c.Name == "sasl" {
					h := s.auth.Handshake()
					s.out <- NewMessage("AUTHENTICATE", h)
				} else if len(s.channels) != 0 && c.Name == "multi-prefix" {
					// TODO merge NAMES commands
					for channel := range s.channels {
						s.out <- NewMessage("NAMES", channel)
					}
				}
			}
		case "NAK":
			// do nothing
		case "NEW":
			for _, c := range ParseCaps(caps) {
				s.availableCaps[c.Name] = c.Value
				if _, ok := SupportedCapabilities[c.Name]; !ok {
					continue
				}
				if _, ok := s.enabledCaps[c.Name]; ok {
					continue
				}
				s.out <- NewMessage("CAP", "REQ", c.Name)
			}
		case "DEL":
			for _, c := range ParseCaps(caps) {
				delete(s.availableCaps, c.Name)
				delete(s.enabledCaps, c.Name)
			}
		}
	case "JOIN":
		if msg.Prefix == nil {
			return nil, errMissingPrefix
		}

		var channel string
		if err := msg.ParseParams(&channel); err != nil {
			return nil, err
		}

		if playback {
			return UserJoinEvent{
				User:    msg.Prefix.Name,
				Channel: channel,
				Time:    msg.TimeOrNow(),
			}, nil
		}

		nickCf := s.Casemap(msg.Prefix.Name)
		channelCf := s.Casemap(channel)

		if s.IsMe(nickCf) {
			s.channels[channelCf] = Channel{
				Name:    msg.Params[0],
				Members: map[*User]string{},
			}
			if _, ok := s.enabledCaps["away-notify"]; ok {
				// Only try to know who is away if the list is
				// updated by the server via away-notify.
				// Otherwise, it'll become outdated over time.
				s.Who(channel)
			}
		} else if c, ok := s.channels[channelCf]; ok {
			if _, ok := s.users[nickCf]; !ok {
				s.users[nickCf] = &User{Name: msg.Prefix.Copy()}
			}
			c.Members[s.users[nickCf]] = ""
			return UserJoinEvent{
				User:    msg.Prefix.Name,
				Channel: c.Name,
				Time:    msg.TimeOrNow(),
			}, nil
		}
	case "PART":
		if msg.Prefix == nil {
			return nil, errMissingPrefix
		}

		var channel string
		if err := msg.ParseParams(&channel); err != nil {
			return nil, err
		}

		if playback {
			return UserPartEvent{
				User:    msg.Prefix.Name,
				Channel: channel,
				Time:    msg.TimeOrNow(),
			}, nil
		}

		nickCf := s.Casemap(msg.Prefix.Name)
		channelCf := s.Casemap(channel)

		if s.IsMe(nickCf) {
			if c, ok := s.channels[channelCf]; ok {
				delete(s.channels, channelCf)
				for u := range c.Members {
					s.cleanUser(u)
				}
				return SelfPartEvent{
					Channel: c.Name,
				}, nil
			}
		} else if c, ok := s.channels[channelCf]; ok {
			if u, ok := s.users[nickCf]; ok {
				delete(c.Members, u)
				s.cleanUser(u)
				s.typings.Done(channelCf, nickCf)
				return UserPartEvent{
					User:    u.Name.Name,
					Channel: c.Name,
					Time:    msg.TimeOrNow(),
				}, nil
			}
		}
	case "KICK":
		var channel, nick string
		if err := msg.ParseParams(&channel, &nick); err != nil {
			return nil, err
		}

		if playback {
			return UserPartEvent{
				User:    nick,
				Channel: channel,
				Time:    msg.TimeOrNow(),
			}, nil
		}

		nickCf := s.Casemap(nick)
		channelCf := s.Casemap(channel)

		if s.IsMe(nickCf) {
			if c, ok := s.channels[channelCf]; ok {
				delete(s.channels, channelCf)
				for u := range c.Members {
					s.cleanUser(u)
				}
				return SelfPartEvent{
					Channel: c.Name,
				}, nil
			}
		} else if c, ok := s.channels[channelCf]; ok {
			if u, ok := s.users[nickCf]; ok {
				delete(c.Members, u)
				s.cleanUser(u)
				s.typings.Done(channelCf, nickCf)
				return UserPartEvent{
					User:    nick,
					Channel: c.Name,
					Time:    msg.TimeOrNow(),
				}, nil
			}
		}
	case "QUIT":
		if msg.Prefix == nil {
			return nil, errMissingPrefix
		}

		if playback {
			return UserQuitEvent{
				User: msg.Prefix.Name,
				Time: msg.TimeOrNow(),
			}, nil
		}

		nickCf := s.Casemap(msg.Prefix.Name)

		if u, ok := s.users[nickCf]; ok {
			u.Disconnected = true
			var channels []string
			for channelCf, c := range s.channels {
				if _, ok := c.Members[u]; ok {
					channels = append(channels, c.Name)
					delete(c.Members, u)
					s.cleanUser(u)
					s.typings.Done(channelCf, nickCf)
				}
			}
			return UserQuitEvent{
				User:     u.Name.Name,
				Channels: channels,
				Time:     msg.TimeOrNow(),
			}, nil
		}
	case rplMononline:
		for _, target := range strings.Split(msg.Params[1], ",") {
			prefix := ParsePrefix(target)
			if prefix == nil {
				continue
			}
			nickCf := s.casemap(prefix.Name)

			if _, ok := s.monitors[nickCf]; ok {
				u, ok := s.users[nickCf]
				if !ok {
					u = &User{
						Name: prefix,
					}
					s.users[nickCf] = u
				}
				if u.Disconnected {
					u.Disconnected = false
					return UserOnlineEvent{
						User: u.Name.Name,
					}, nil
				}
			}
		}
	case rplMonoffline:
		for _, target := range strings.Split(msg.Params[1], ",") {
			prefix := ParsePrefix(target)
			if prefix == nil {
				continue
			}
			nickCf := s.casemap(prefix.Name)

			if _, ok := s.monitors[nickCf]; ok {
				u, ok := s.users[nickCf]
				if !ok {
					u = &User{
						Name: prefix,
					}
					s.users[nickCf] = u
				}
				if !u.Disconnected {
					u.Disconnected = true
					return UserOfflineEvent{
						User: u.Name.Name,
					}, nil
				}
			}
		}
	case rplNamreply:
		var channel, names string
		if err := msg.ParseParams(nil, nil, &channel, &names); err != nil {
			return nil, err
		}

		channelCf := s.Casemap(channel)

		if c, ok := s.channels[channelCf]; ok {

			for _, name := range ParseNameReply(names, s.prefixSymbols) {
				nickCf := s.Casemap(name.Name.Name)

				if _, ok := s.users[nickCf]; !ok {
					s.users[nickCf] = &User{Name: name.Name.Copy()}
				}
				c.Members[s.users[nickCf]] = name.PowerLevel
			}

			s.channels[channelCf] = c
		}
	case rplEndofnames:
		var channel string
		if err := msg.ParseParams(nil, &channel); err != nil {
			return nil, err
		}

		channelCf := s.Casemap(channel)

		if c, ok := s.channels[channelCf]; ok && !c.complete {
			c.complete = true
			s.channels[channelCf] = c
			ev := SelfJoinEvent{
				Channel: c.Name,
				Topic:   c.Topic,
			}
			if stamp, ok := s.pendingChannels[channelCf]; ok && time.Since(stamp) < 5*time.Second {
				ev.Requested = true
			}
			return ev, nil
		}
	case rplTopic:
		var channel, topic string
		if err := msg.ParseParams(nil, &channel, &topic); err != nil {
			return nil, err
		}

		channelCf := s.Casemap(channel)

		if c, ok := s.channels[channelCf]; ok {
			c.Topic = topic
			s.channels[channelCf] = c
		}
	case rplTopicwhotime:
		var channel, topicWho, topicTime string
		if err := msg.ParseParams(nil, &channel, &topicWho, &topicTime); err != nil {
			return nil, err
		}

		channelCf := s.Casemap(channel)

		// ignore the error, we still have topicWho
		t, _ := strconv.ParseInt(topicTime, 10, 64)

		if c, ok := s.channels[channelCf]; ok {
			c.TopicWho = ParsePrefix(topicWho)
			c.TopicTime = time.Unix(t, 0)
			s.channels[channelCf] = c
		}
	case rplNotopic:
		var channel string
		if err := msg.ParseParams(nil, &channel); err != nil {
			return nil, err
		}

		channelCf := s.Casemap(channel)

		if c, ok := s.channels[channelCf]; ok {
			c.Topic = ""
			s.channels[channelCf] = c
		}
	case "TOPIC":
		if msg.Prefix == nil {
			return nil, errMissingPrefix
		}

		var channel, topic string
		if err := msg.ParseParams(&channel, &topic); err != nil {
			return nil, err
		}

		if playback {
			return TopicChangeEvent{
				Channel: channel,
				Topic:   topic,
				Time:    msg.TimeOrNow(),
			}, nil
		}

		channelCf := s.Casemap(channel)

		if c, ok := s.channels[channelCf]; ok {
			c.Topic = topic
			c.TopicWho = msg.Prefix.Copy()
			c.TopicTime = msg.TimeOrNow()
			s.channels[channelCf] = c
			return TopicChangeEvent{
				Channel: c.Name,
				Topic:   c.Topic,
				Time:    msg.TimeOrNow(),
			}, nil
		}
	case "MODE":
		var channel string
		if err := msg.ParseParams(&channel, nil); err != nil {
			return nil, err
		}
		mode := strings.Join(msg.Params[1:], " ")

		if playback {
			return ModeChangeEvent{
				Channel: channel,
				Mode:    mode,
				Time:    msg.TimeOrNow(),
			}, nil
		}

		channelCf := s.Casemap(channel)

		if c, ok := s.channels[channelCf]; ok {
			modeChanges, err := ParseChannelMode(msg.Params[1], msg.Params[2:], s.chanmodes, s.prefixModes)
			if err != nil {
				return nil, err
			}
			for _, change := range modeChanges {
				i := strings.IndexByte(s.prefixModes, change.Mode)
				if i < 0 {
					continue
				}
				nickCf := s.Casemap(change.Param)
				user := s.users[nickCf]
				membership, ok := c.Members[user]
				if !ok {
					continue
				}
				var newMembership []byte
				if change.Enable {
					newMembership = append([]byte(membership), s.prefixSymbols[i])
					sort.Slice(newMembership, func(i, j int) bool {
						i = strings.IndexByte(s.prefixSymbols, newMembership[i])
						j = strings.IndexByte(s.prefixSymbols, newMembership[j])
						return i < j
					})
				} else if j := strings.IndexByte(membership, s.prefixSymbols[i]); j >= 0 {
					newMembership = []byte(membership)
					newMembership = append(newMembership[:j], newMembership[j+1:]...)
				}
				c.Members[user] = string(newMembership)
			}
			s.channels[channelCf] = c
			return ModeChangeEvent{
				Channel: c.Name,
				Mode:    mode,
				Time:    msg.TimeOrNow(),
			}, nil
		}
	case "INVITE":
		if msg.Prefix == nil {
			return nil, errMissingPrefix
		}

		var nick, channel string
		if err := msg.ParseParams(&nick, &channel); err != nil {
			return nil, err
		}

		return InviteEvent{
			Inviter: msg.Prefix.Name,
			Invitee: nick,
			Channel: channel,
		}, nil
	case rplInviting:
		var nick, channel string
		if err := msg.ParseParams(nil, &nick, &channel); err != nil {
			return nil, err
		}

		return InviteEvent{
			Inviter: s.nick,
			Invitee: nick,
			Channel: channel,
		}, nil
	case "AWAY":
		if msg.Prefix == nil {
			return nil, errMissingPrefix
		}

		nickCf := s.Casemap(msg.Prefix.Name)

		if u, ok := s.users[nickCf]; ok {
			u.Away = len(msg.Params) == 1
		}
	case "PRIVMSG", "NOTICE":
		if msg.Prefix == nil {
			return nil, errMissingPrefix
		}

		var target string
		if err := msg.ParseParams(&target); err != nil {
			return nil, err
		}

		if playback {
			return s.newMessageEvent(msg)
		}

		targetCf := s.casemap(target)
		nickCf := s.casemap(msg.Prefix.Name)
		s.typings.Done(targetCf, nickCf)

		return s.newMessageEvent(msg)
	case "TAGMSG":
		if playback {
			return nil, nil
		}

		if msg.Prefix == nil {
			return nil, errMissingPrefix
		}

		var target string
		if err := msg.ParseParams(&target); err != nil {
			return nil, err
		}

		targetCf := s.casemap(target)
		nickCf := s.casemap(msg.Prefix.Name)

		if s.IsMe(msg.Prefix.Name) {
			// TAGMSG from self
			break
		}

		if t, ok := msg.Tags["+typing"]; ok {
			switch t {
			case "active":
				s.typings.Active(targetCf, nickCf)
			case "paused", "done":
				s.typings.Done(targetCf, nickCf)
			}
		}
	case "BATCH":
		var id string
		if err := msg.ParseParams(&id); err != nil {
			return nil, err
		}
		if len(id) == 0 {
			return nil, fmt.Errorf("empty batch id")
		}

		batchStart := id[0] == '+'
		id = id[1:]

		if batchStart {
			var name string
			if err := msg.ParseParams(nil, &name); err != nil {
				return nil, err
			}

			switch name {
			case "chathistory":
				var target string
				if err := msg.ParseParams(nil, nil, &target); err != nil {
					return nil, err
				}

				s.chBatches[id] = HistoryEvent{Target: target}
			case "draft/chathistory-targets":
				s.targetsBatchID = id
				s.targetsBatch = HistoryTargetsEvent{Targets: make(map[string]time.Time)}
			case "soju.im/search":
				s.searchBatchID = id
				s.searchBatch = SearchEvent{}
			}
		} else {
			if b, ok := s.chBatches[id]; ok {
				delete(s.chBatches, id)
				delete(s.chReqs, s.Casemap(b.Target))
				return b, nil
			} else if s.targetsBatchID == id {
				s.targetsBatchID = ""
				delete(s.chReqs, "")
				return s.targetsBatch, nil
			} else if s.searchBatchID == id {
				s.searchBatchID = ""
				return s.searchBatch, nil
			}
		}
	case "NICK":
		if msg.Prefix == nil {
			return nil, errMissingPrefix
		}

		var nick string
		if err := msg.ParseParams(&nick); err != nil {
			return nil, err
		}

		if playback {
			return UserNickEvent{
				User:       nick,
				FormerNick: msg.Prefix.Name,
				Time:       msg.TimeOrNow(),
			}, nil
		}

		nickCf := s.Casemap(msg.Prefix.Name)
		newNick := nick
		newNickCf := s.Casemap(newNick)

		if formerUser, ok := s.users[nickCf]; ok {
			formerUser.Name.Name = newNick
			delete(s.users, nickCf)
			s.users[newNickCf] = formerUser
		} else {
			break
		}

		if s.IsMe(msg.Prefix.Name) {
			s.nick = newNick
			s.nickCf = newNickCf
			return SelfNickEvent{
				FormerNick: msg.Prefix.Name,
			}, nil
		} else {
			return UserNickEvent{
				User:       nick,
				FormerNick: msg.Prefix.Name,
				Time:       msg.TimeOrNow(),
			}, nil
		}
	case "MARKREAD":
		if len(msg.Params) < 2 {
			break
		}
		var target, timestamp string
		if err := msg.ParseParams(&target, &timestamp); err != nil {
			return nil, err
		}
		if !strings.HasPrefix(timestamp, "timestamp=") {
			return nil, nil
		}
		timestamp = strings.TrimPrefix(timestamp, "timestamp=")
		t, ok := parseTimestamp(timestamp)
		if !ok {
			return nil, nil
		}
		return ReadEvent{
			Target:    target,
			Timestamp: t,
		}, nil
	case "BOUNCER":
		if len(msg.Params) < 3 {
			break
		}
		if msg.Params[0] != "NETWORK" || s.netID != "" {
			break
		}
		id := msg.Params[1]
		event := BouncerNetworkEvent{
			ID: id,
		}
		if msg.Params[2] != "*" {
			attrs := parseTags(msg.Params[2])
			event.Name = attrs["name"]
		} else {
			event.Delete = true
		}
		return event, nil
	case "PING":
		var payload string
		if err := msg.ParseParams(&payload); err != nil {
			return nil, err
		}

		s.out <- NewMessage("PONG", payload)
	case "ERROR":
		s.Close()
	case "FAIL", "WARN", "NOTE":
		var severity Severity
		var code string
		if err := msg.ParseParams(nil, &code); err != nil {
			return nil, err
		}

		switch msg.Command {
		case "FAIL":
			severity = SeverityFail
		case "WARN":
			severity = SeverityWarn
		case "NOTE":
			severity = SeverityNote
		}

		return ErrorEvent{
			Severity: severity,
			Code:     code,
			Message:  strings.Join(msg.Params[2:], " "),
		}, nil
	case errMonlistisfull:
		// silence monlist full error, we don't care because we do it best-effort
	case rplAway:
		// we display user away status, we don't care about automatic AWAY replies
	default:
		if msg.IsReply() {
			if len(msg.Params) < 2 {
				return nil, msg.errNotEnoughParams(2)
			}
			if msg.Command == rplUmodeis && !s.receivedUserMode {
				// ignore the first RPL_UMODEIS on join
				s.receivedUserMode = true
				return nil, nil

			}
			if msg.Command == errUnknowncommand && msg.Params[1] == "BOUNCER" {
				// ignore any error in response to unconditional BOUNCER LISTNETWORKS
				return nil, nil
			}
			return ErrorEvent{
				Severity: ReplySeverity(msg.Command),
				Code:     msg.Command,
				Message:  strings.Join(msg.Params[1:], " "),
			}, nil
		}
	}
	return nil, nil
}

func (s *Session) newMessageEvent(msg Message) (ev MessageEvent, err error) {
	if msg.Prefix == nil {
		return ev, errMissingPrefix
	}

	var target, content string
	if err := msg.ParseParams(&target, &content); err != nil {
		return ev, err
	}

	ev = MessageEvent{
		User:    msg.Prefix.Name, // TODO correctly casemap
		Target:  target,          // TODO correctly casemap
		Command: msg.Command,
		Content: content,
		Time:    msg.TimeOrNow(),
	}

	if s.IsMe(target) {
		if context := msg.Tags["+draft/channel-context"]; context != "" {
			target = context
		}
	}
	targetCf := s.Casemap(target)
	if c, ok := s.channels[targetCf]; ok {
		ev.Target = c.Name
		ev.TargetIsChannel = true
	}

	return ev, nil
}

func (s *Session) cleanUser(parted *User) {
	nameCf := s.Casemap(parted.Name.Name)
	if _, ok := s.monitors[nameCf]; ok {
		return
	}
	for _, c := range s.channels {
		if _, ok := c.Members[parted]; ok {
			return
		}
	}
	delete(s.users, nameCf)
}

func (s *Session) updateFeatures(features []string) {
	for _, f := range features {
		if f == "" || f == "-" || f == "=" || f == "-=" {
			continue
		}

		var (
			add   bool
			key   string
			value string
		)

		if strings.HasPrefix(f, "-") {
			add = false
			f = f[1:]
		} else {
			add = true
		}

		kv := strings.SplitN(f, "=", 2)
		key = strings.ToUpper(kv[0])
		if len(kv) > 1 {
			value = kv[1]
		}

		if !add {
			// TODO support ISUPPORT negations
			continue
		}

	Switch:
		switch key {
		case "BOUNCER_NETID":
			s.netID = value
		case "CASEMAPPING":
			switch value {
			case "ascii":
				s.casemap = CasemapASCII
			default:
				s.casemap = CasemapRFC1459
			}
		case "CHANMODES":
			// We only care about the first four params
			types := strings.SplitN(value, ",", 5)
			for i := 0; i < len(types) && i < len(s.chanmodes); i++ {
				s.chanmodes[i] = types[i]
			}
		case "CHANTYPES":
			s.chantypes = value
		case "CHATHISTORY":
			historyLimit, err := strconv.Atoi(value)
			if err == nil {
				s.historyLimit = historyLimit
			}
		case "LINELEN":
			linelen, err := strconv.Atoi(value)
			if err == nil && linelen != 0 {
				s.linelen = linelen
			}
		case "MONITOR":
			monitor, err := strconv.Atoi(value)
			if err == nil && monitor > 0 {
				s.monitor = true
			}
		case "PREFIX":
			if value == "" {
				s.prefixModes = ""
				s.prefixSymbols = ""
				break Switch
			}
			if len(value)%2 != 0 {
				break Switch
			}
			for i := 0; i < len(value); i++ {
				if unicode.MaxASCII < value[i] {
					break Switch
				}
			}
			numPrefixes := len(value)/2 - 1
			s.prefixModes = value[1 : numPrefixes+1]
			s.prefixSymbols = value[numPrefixes+2:]
		case "WHOX":
			s.whox = true
		}
	}
}

func (s *Session) endRegistration() {
	if s.registered {
		return
	}
	if s.netID != "" {
		s.out <- NewMessage("BOUNCER", "BIND", s.netID)
		s.out <- NewMessage("CAP", "END")
	} else {
		s.out <- NewMessage("CAP", "END")
		s.out <- NewMessage("BOUNCER", "LISTNETWORKS")
	}
}