master sewn/kohai / irc / session.go
   1package irc
   2
   3import (
   4	"bytes"
   5	"encoding/base64"
   6	"errors"
   7	"fmt"
   8	"sort"
   9	"strconv"
  10	"strings"
  11	"time"
  12	"unicode"
  13
  14	"git.sr.ht/~rockorager/vaxis"
  15	"golang.org/x/time/rate"
  16)
  17
  18type SASLClient interface {
  19	Early() bool
  20	Handshake() (mech string)
  21	Respond(challenge string) (res string, err error)
  22}
  23
  24type SASLPlain struct {
  25	Username string
  26	Password string
  27}
  28
  29func (auth *SASLPlain) Early() bool {
  30	return true
  31}
  32
  33func (auth *SASLPlain) Handshake() (mech string) {
  34	mech = "PLAIN"
  35	return
  36}
  37
  38func (auth *SASLPlain) Respond(challenge string) (res string, err error) {
  39	if challenge != "+" {
  40		err = errors.New("unexpected challenge")
  41		return
  42	}
  43
  44	user := []byte(auth.Username)
  45	pass := []byte(auth.Password)
  46	payload := bytes.Join([][]byte{user, user, pass}, []byte{0})
  47	res = base64.StdEncoding.EncodeToString(payload)
  48
  49	return
  50}
  51
  52// SupportedCapabilities is the set of capabilities supported by this library.
  53// Value is false if the cap is deferred (to work around some daemons agfressive rate pre-conn-reg backlog limiting)
  54var SupportedCapabilities = map[string]bool{
  55	"away-notify":      false,
  56	"batch":            true,
  57	"cap-notify":       true,
  58	"echo-message":     true,
  59	"extended-monitor": false,
  60	"invite-notify":    false,
  61	"labeled-response": true,
  62	"message-tags":     true,
  63	"multi-prefix":     true,
  64	"sasl":             true,
  65	"server-time":      true,
  66	"setname":          false,
  67	"standard-replies": true,
  68
  69	"draft/chathistory":               true,
  70	"draft/event-playback":            true,
  71	"draft/metadata-2":                true,
  72	"draft/read-marker":               true,
  73	"soju.im/bouncer-networks-notify": true,
  74	"soju.im/bouncer-networks":        true,
  75	"soju.im/search":                  false,
  76}
  77
  78// Values taken by the "@+typing=" client tag.  TypingUnspec means the value or
  79// tag is absent.
  80const (
  81	TypingUnspec = iota
  82	TypingActive
  83	TypingPaused
  84	TypingDone
  85)
  86
  87// User is a known IRC user.
  88type User struct {
  89	Name         *Prefix // the nick, user and hostname of the user if known.
  90	Away         bool    // whether the user is away or not
  91	Disconnected bool    // can only be true for monitored users.
  92}
  93
  94type ChannelMember struct {
  95	Membership string
  96	LastActive time.Time
  97}
  98
  99// Channel is a joined channel.
 100type Channel struct {
 101	Name      string                  // the name of the channel.
 102	Members   map[*User]ChannelMember // the set of members associated with their membership.
 103	Topic     string                  // the topic of the channel, or "" if absent.
 104	TopicWho  *Prefix                 // the name of the last user who set the topic.
 105	TopicTime time.Time               // the last time the topic has been changed.
 106	Read      time.Time               // the time until which messages were read.
 107
 108	complete bool // whether this structure is fully initialized.
 109}
 110
 111type Metadata struct {
 112	Pinned bool
 113	Muted  bool
 114}
 115
 116// SessionParams defines how to connect to an IRC server.
 117type SessionParams struct {
 118	Nickname string
 119	Username string
 120	RealName string
 121	NetID    string
 122	Auth     SASLClient
 123}
 124
 125type Session struct {
 126	out          chan<- Message
 127	closed       bool
 128	registered   bool
 129	typings      *Typings               // incoming typing notifications.
 130	typingStamps map[string]typingStamp // user typing instants.
 131
 132	nick   string
 133	nickCf string // casemapped nickname.
 134	user   string
 135	real   string
 136	acct   string
 137	host   string
 138	netID  string
 139	auth   SASLClient
 140
 141	availableCaps map[string]string
 142	enabledCaps   map[string]struct{}
 143	metadataSubs  map[string]struct{}
 144
 145	serverName    string
 146	defaultPrefix *Prefix
 147
 148	// ISUPPORT features
 149	casemap       func(string) string
 150	chanmodes     [4]string
 151	chantypes     string
 152	linelen       int
 153	historyLimit  int
 154	prefixSymbols string
 155	prefixModes   string
 156	statusmsg     string
 157	monitor       bool
 158	whox          bool
 159	listMask      bool
 160	upload        string
 161
 162	clientTagListIsAllow bool // whether clientTagList is an allowlist (true) or a blocklist (false)
 163	clientTagList        map[string]struct{}
 164
 165	users           map[string]*User        // known users.
 166	channels        map[string]Channel      // joined channels.
 167	metadata        map[string]Metadata     // known target metadata.
 168	chBatches       map[string]HistoryEvent // channel history batches being processed.
 169	chReqs          map[string]struct{}     // set of targets for which history is currently requested.
 170	targetsBatchID  string                  // ID of the channel history targets batch being processed.
 171	targetsBatch    HistoryTargetsEvent     // channel history targets batch being processed.
 172	searchBatchID   string                  // ID of the search targets batch being processed.
 173	searchBatch     SearchEvent             // search batch being processed.
 174	monitors        map[string]struct{}     // set of users we want to monitor (and keep even if they are disconnected).
 175	pendingList     ListEvent               // current list response being received (flushed on list end).
 176	pendingChannels map[string]time.Time    // set of join requests stamps for channels.
 177
 178	receivedISupport bool
 179	receivedUserMode bool
 180}
 181
 182func NewSession(out chan<- Message, params SessionParams) *Session {
 183	s := &Session{
 184		out:             out,
 185		typings:         NewTypings(),
 186		typingStamps:    map[string]typingStamp{},
 187		nick:            params.Nickname,
 188		nickCf:          CasemapASCII(params.Nickname),
 189		user:            params.Username,
 190		real:            params.RealName,
 191		netID:           params.NetID,
 192		auth:            params.Auth,
 193		availableCaps:   map[string]string{},
 194		enabledCaps:     map[string]struct{}{},
 195		metadataSubs:    map[string]struct{}{},
 196		casemap:         CasemapRFC1459,
 197		chantypes:       "#&",
 198		linelen:         512,
 199		historyLimit:    100,
 200		prefixSymbols:   "@+",
 201		prefixModes:     "ov",
 202		clientTagList:   map[string]struct{}{},
 203		users:           map[string]*User{},
 204		channels:        map[string]Channel{},
 205		metadata:        map[string]Metadata{},
 206		chBatches:       map[string]HistoryEvent{},
 207		chReqs:          map[string]struct{}{},
 208		monitors:        map[string]struct{}{},
 209		pendingChannels: map[string]time.Time{},
 210	}
 211
 212	s.out <- NewMessage("CAP", "LS", "302")
 213	for capability, immediate := range SupportedCapabilities {
 214		if immediate || s.netID != "" {
 215			s.out <- NewMessage("CAP", "REQ", capability)
 216		}
 217	}
 218	s.out <- NewMessage("NICK", s.nick)
 219	s.out <- NewMessage("USER", s.user, "0", "*", s.real)
 220	if s.auth != nil && s.auth.Early() {
 221		h := s.auth.Handshake()
 222		s.out <- NewMessage("AUTHENTICATE", h)
 223		res, err := s.auth.Respond("+")
 224		if err != nil {
 225			s.out <- NewMessage("AUTHENTICATE", "*")
 226		} else {
 227			s.out <- NewMessage("AUTHENTICATE", res)
 228		}
 229		s.auth = nil
 230	}
 231
 232	if s.auth == nil {
 233		s.endRegistration()
 234	}
 235
 236	return s
 237}
 238
 239func (s *Session) Close() {
 240	if s.closed {
 241		return
 242	}
 243	s.closed = true
 244	s.typings.Close()
 245	close(s.out)
 246}
 247
 248// HasCapability reports whether the given capability has been negotiated
 249// successfully.
 250func (s *Session) HasCapability(capability string) bool {
 251	_, ok := s.enabledCaps[capability]
 252	return ok
 253}
 254
 255func (s *Session) CanSendTag(clientTagWithoutThePlus string) bool {
 256	_, ok := s.clientTagList[clientTagWithoutThePlus]
 257	return ok == s.clientTagListIsAllow
 258}
 259
 260func (s *Session) IsBouncer() bool {
 261	if s.HasCapability("soju.im/bouncer-networks") { // soju-compatible
 262		return true
 263	}
 264	if s.defaultPrefix != nil && s.defaultPrefix.Name == "irc.znc.in" { // ZNC
 265		return true
 266	}
 267	return false
 268}
 269
 270// BouncerService returns the optional nick of the bouncer service user.
 271func (s *Session) BouncerService() string {
 272	switch s.serverName {
 273	case "soju":
 274		return "BouncerServ"
 275	}
 276	return ""
 277}
 278
 279// UploadURL returns the URL to which files can be uploaded according to the FILEHOST specification.
 280func (s *Session) UploadURL() string {
 281	return s.upload
 282}
 283
 284func (s *Session) HasListMask() bool {
 285	return s.listMask
 286}
 287
 288func (s *Session) Nick() string {
 289	return s.nick
 290}
 291
 292func (s *Session) NetID() string {
 293	return s.netID
 294}
 295
 296// NickCf is our casemapped nickname.
 297func (s *Session) NickCf() string {
 298	return s.nickCf
 299}
 300
 301func (s *Session) IsMe(nick string) bool {
 302	return s.nickCf == s.casemap(nick)
 303}
 304
 305func (s *Session) IsChannel(name string) bool {
 306	return strings.IndexAny(name, s.chantypes) == 0
 307}
 308
 309func (s *Session) Casemap(name string) string {
 310	return s.casemap(name)
 311}
 312
 313// Membership returns the prefix-symbol string for nick's membership in the
 314// given channel (e.g. "@", "+", "@+"), or "" if the target is not a known
 315// channel or the nick is not a member. The string is ordered highest-priority
 316// first, so callers wanting the single highest symbol can take string[:1].
 317func (s *Session) Membership(channel, nick string) string {
 318	c, ok := s.channels[s.Casemap(channel)]
 319	if !ok {
 320		return ""
 321	}
 322	u, ok := s.users[s.Casemap(nick)]
 323	if !ok {
 324		return ""
 325	}
 326	m, ok := c.Members[u]
 327	if !ok {
 328		return ""
 329	}
 330	return m.Membership
 331}
 332
 333// Users returns the list of all known nicknames.
 334func (s *Session) Users() []string {
 335	users := make([]string, 0, len(s.users))
 336	for _, u := range s.users {
 337		users = append(users, u.Name.Name)
 338	}
 339	return users
 340}
 341
 342// Names returns the list of users in the given target, or nil if the target
 343// is not a known channel or nick in the session.
 344// The list is sorted according to member name.
 345func (s *Session) Names(target string) []Member {
 346	var names []Member
 347	if s.IsChannel(target) {
 348		if c, ok := s.channels[s.Casemap(target)]; ok {
 349			names = make([]Member, 0, len(c.Members))
 350			for u, m := range c.Members {
 351				names = append(names, Member{
 352					PowerLevel:   m.Membership,
 353					Name:         u.Name.Copy(),
 354					Away:         u.Away,
 355					Disconnected: u.Disconnected,
 356					Self:         s.nickCf == s.casemap(u.Name.Name),
 357					LastActive:   m.LastActive,
 358				})
 359			}
 360		}
 361	} else if u, ok := s.users[s.Casemap(target)]; ok {
 362		names = append(names, Member{
 363			Name:         u.Name.Copy(),
 364			Away:         u.Away,
 365			Disconnected: u.Disconnected,
 366		})
 367		names = append(names, Member{
 368			Name: &Prefix{
 369				Name: s.nick,
 370			},
 371			Self: true,
 372		})
 373	}
 374	sort.Sort(members{
 375		m:        names,
 376		prefixes: s.prefixSymbols,
 377	})
 378	return names
 379}
 380
 381// Typings returns the list of nickname who are currently typing.
 382func (s *Session) Typings(target string) []string {
 383	targetCf := s.casemap(target)
 384	res := s.typings.List(targetCf)
 385	for i := 0; i < len(res); i++ {
 386		if s.IsMe(res[i]) {
 387			res = append(res[:i], res[i+1:]...)
 388			i--
 389		} else if u, ok := s.users[res[i]]; ok {
 390			res[i] = u.Name.Name
 391		}
 392	}
 393	sort.Strings(res)
 394	return res
 395}
 396
 397func (s *Session) TypingStops() <-chan Typing {
 398	return s.typings.Stops()
 399}
 400
 401func (s *Session) ChannelsSharedWith(name string) []string {
 402	var user *User
 403	if u, ok := s.users[s.Casemap(name)]; ok && !u.Disconnected {
 404		user = u
 405	} else {
 406		return nil
 407	}
 408	var channels []string
 409	for _, c := range s.channels {
 410		if _, ok := c.Members[user]; ok {
 411			channels = append(channels, c.Name)
 412		}
 413	}
 414	return channels
 415}
 416
 417func (s *Session) Topic(channel string) (topic string, who *Prefix, at time.Time) {
 418	channelCf := s.Casemap(channel)
 419	if c, ok := s.channels[channelCf]; ok {
 420		topic = c.Topic
 421		who = c.TopicWho
 422		at = c.TopicTime
 423	}
 424	return
 425}
 426
 427func (s *Session) SendRaw(raw string) {
 428	s.out <- NewMessage(raw)
 429}
 430
 431func (s *Session) Send(command string, params ...string) {
 432	s.out <- NewMessage(command, params...)
 433}
 434
 435func (s *Session) List(pattern string) {
 436	if pattern != "" {
 437		s.out <- NewMessage("LIST", pattern)
 438	} else {
 439		s.out <- NewMessage("LIST")
 440	}
 441}
 442
 443func (s *Session) Join(channel, key string) {
 444	channelCf := s.Casemap(channel)
 445	s.pendingChannels[channelCf] = time.Now()
 446	if key == "" {
 447		s.out <- NewMessage("JOIN", channel)
 448	} else {
 449		s.out <- NewMessage("JOIN", channel, key)
 450	}
 451}
 452
 453func (s *Session) Part(channel, reason string) {
 454	s.out <- NewMessage("PART", channel, reason)
 455}
 456
 457func (s *Session) ChangeTopic(channel, topic string) {
 458	s.out <- NewMessage("TOPIC", channel, topic)
 459}
 460
 461func (s *Session) Quit(reason string) {
 462	s.out <- NewMessage("QUIT", reason)
 463}
 464
 465func (s *Session) ChangeNick(nick string) {
 466	s.out <- NewMessage("NICK", nick)
 467}
 468
 469func (s *Session) Who(target string) {
 470	if s.whox {
 471		// only request what we need, to optimize server who cache hits and reduce traffic
 472		s.out <- NewMessage("WHO", target, "%uhnf")
 473	} else {
 474		s.out <- NewMessage("WHO", target)
 475	}
 476}
 477
 478func (s *Session) ChangeMode(channel, flags string, args []string) {
 479	if flags != "" {
 480		args = append([]string{channel, flags}, args...)
 481	} else {
 482		args = append([]string{channel}, args...)
 483	}
 484	s.out <- NewMessage("MODE", args...)
 485}
 486
 487func (s *Session) Search(target, text string) {
 488	if _, ok := s.enabledCaps["soju.im/search"]; !ok {
 489		return
 490	}
 491	attrs := make(map[string]string)
 492	attrs["text"] = text
 493	if target != "" {
 494		attrs["in"] = target
 495	}
 496	s.out <- NewMessage("SEARCH", formatTags(attrs))
 497}
 498
 499func (s *Session) Away(message string) {
 500	if message != "" {
 501		s.out <- NewMessage("AWAY", message)
 502	} else {
 503		s.out <- NewMessage("AWAY")
 504	}
 505}
 506
 507func splitChunks(s string, chunkLen int) (chunks []string) {
 508	if chunkLen <= 0 || len(s) <= chunkLen {
 509		return []string{s}
 510	}
 511
 512	b := 0
 513	n := 0
 514	for _, c := range vaxis.Characters(s) {
 515		cw := len(c.Grapheme)
 516		if n+cw > chunkLen {
 517			chunks = append(chunks, s[b:b+n])
 518			b += n
 519			n = cw
 520			continue
 521		}
 522		n += cw
 523	}
 524	if b < len(s) {
 525		chunks = append(chunks, s[b:])
 526	}
 527	return
 528}
 529
 530func (s *Session) PrivMsg(target, content string) {
 531	hostLen := len(s.host)
 532	if hostLen == 0 {
 533		hostLen = len("255.255.255.255")
 534	}
 535	maxMessageLen := s.linelen -
 536		len(":!@ PRIVMSG  :\r\n") -
 537		len(s.nick) -
 538		len(s.user) -
 539		hostLen -
 540		len(target)
 541	chunks := splitChunks(content, maxMessageLen)
 542	for _, chunk := range chunks {
 543		s.out <- NewMessage("PRIVMSG", target, chunk)
 544	}
 545	targetCf := s.Casemap(target)
 546	delete(s.typingStamps, targetCf)
 547}
 548
 549func (s *Session) Typing(target string) {
 550	if !s.HasCapability("message-tags") || !s.CanSendTag("typing") {
 551		return
 552	}
 553	targetCf := s.casemap(target)
 554	now := time.Now()
 555	t, ok := s.typingStamps[targetCf]
 556	if ok && ((t.Type == TypingActive && now.Sub(t.Last).Seconds() < 3.0) || !t.Limit.Allow()) {
 557		return
 558	}
 559	if !ok {
 560		t.Limit = rate.NewLimiter(rate.Limit(1.0/3.0), 5)
 561		t.Limit.Reserve() // will always be OK
 562	}
 563	s.typingStamps[targetCf] = typingStamp{
 564		Last:  now,
 565		Type:  TypingActive,
 566		Limit: t.Limit,
 567	}
 568	s.out <- NewMessage("TAGMSG", target).WithTag("+typing", "active")
 569}
 570
 571func (s *Session) TypingStop(target string) {
 572	if !s.HasCapability("message-tags") || !s.CanSendTag("typing") {
 573		return
 574	}
 575	targetCf := s.casemap(target)
 576	now := time.Now()
 577	t, ok := s.typingStamps[targetCf]
 578	if ok && (t.Type == TypingDone || !t.Limit.Allow()) {
 579		// don't send a +typing=done again if the last typing we sent was a +typing=done
 580		return
 581	}
 582	if !ok {
 583		t.Limit = rate.NewLimiter(rate.Limit(1), 5)
 584		t.Limit.Reserve() // will always be OK
 585	}
 586	s.typingStamps[targetCf] = typingStamp{
 587		Last:  now,
 588		Type:  TypingDone,
 589		Limit: t.Limit,
 590	}
 591	s.out <- NewMessage("TAGMSG", target).WithTag("+typing", "done")
 592}
 593
 594func (s *Session) ReadGet(target string) {
 595	if _, ok := s.enabledCaps["draft/read-marker"]; ok {
 596		s.out <- NewMessage("MARKREAD", target)
 597	}
 598}
 599
 600func (s *Session) ReadSet(target string, timestamp time.Time) {
 601	if _, ok := s.enabledCaps["draft/read-marker"]; ok {
 602		s.out <- NewMessage("MARKREAD", target, formatTimestamp(timestamp))
 603	}
 604}
 605
 606func (s *Session) MutedGet(target string) bool {
 607	return s.metadata[s.Casemap(target)].Muted
 608}
 609
 610func (s *Session) MutedSet(target string, muted bool) (ok bool) {
 611	var v string
 612	if muted {
 613		v = "1"
 614	} else {
 615		v = "0"
 616	}
 617	k := "soju.im/muted"
 618	if _, ok = s.metadataSubs[k]; ok {
 619		s.out <- NewMessage("METADATA", target, "SET", k, v)
 620	}
 621	return
 622}
 623
 624func (s *Session) PinnedGet(target string) bool {
 625	return s.metadata[s.Casemap(target)].Pinned
 626}
 627
 628func (s *Session) PinnedSet(target string, pinned bool) (ok bool) {
 629	var v string
 630	if pinned {
 631		v = "1"
 632	} else {
 633		v = "0"
 634	}
 635	k := "soju.im/pinned"
 636	if _, ok = s.metadataSubs[k]; ok {
 637		s.out <- NewMessage("METADATA", target, "SET", "soju.im/pinned", v)
 638	}
 639	return
 640}
 641
 642func (s *Session) MonitorAdd(target string) {
 643	targetCf := s.casemap(target)
 644	if _, ok := s.monitors[targetCf]; !ok {
 645		s.monitors[targetCf] = struct{}{}
 646		if s.monitor {
 647			s.out <- NewMessage("MONITOR", "+", target)
 648		}
 649	}
 650}
 651
 652func (s *Session) MonitorRemove(target string) {
 653	targetCf := s.casemap(target)
 654	if _, ok := s.monitors[targetCf]; ok {
 655		delete(s.monitors, targetCf)
 656		if s.monitor {
 657			s.out <- NewMessage("MONITOR", "-", target)
 658		}
 659	}
 660}
 661
 662type HistoryRequest struct {
 663	s       *Session
 664	target  string
 665	command string
 666	bounds  []string
 667	limit   int
 668}
 669
 670func formatTimestamp(t time.Time) string {
 671	t = t.UTC()
 672	return fmt.Sprintf("timestamp=%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
 673		t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()/1e6)
 674}
 675
 676func (r *HistoryRequest) WithLimit(limit int) *HistoryRequest {
 677	if limit < r.s.historyLimit {
 678		r.limit = limit
 679	} else {
 680		r.limit = r.s.historyLimit
 681	}
 682	return r
 683}
 684
 685func (r *HistoryRequest) doRequest() {
 686	if !r.s.HasCapability("draft/chathistory") {
 687		return
 688	}
 689
 690	targetCf := r.s.casemap(r.target)
 691	if _, ok := r.s.chReqs[targetCf]; ok {
 692		return
 693	}
 694	r.s.chReqs[targetCf] = struct{}{}
 695
 696	args := make([]string, 0, len(r.bounds)+3)
 697	args = append(args, r.command)
 698	if r.target != "" {
 699		args = append(args, r.target)
 700	}
 701	args = append(args, r.bounds...)
 702	args = append(args, strconv.Itoa(r.limit))
 703	r.s.out <- NewMessage("CHATHISTORY", args...)
 704}
 705
 706func (r *HistoryRequest) After(t time.Time) {
 707	r.command = "AFTER"
 708	r.bounds = []string{formatTimestamp(t)}
 709	r.doRequest()
 710}
 711
 712func (r *HistoryRequest) Before(t time.Time) {
 713	r.command = "BEFORE"
 714	r.bounds = []string{formatTimestamp(t)}
 715	r.doRequest()
 716}
 717
 718func (r *HistoryRequest) Latest() {
 719	r.command = "LATEST"
 720	r.bounds = []string{"*"}
 721	r.doRequest()
 722}
 723
 724func (r *HistoryRequest) Targets(start time.Time, end time.Time) {
 725	r.command = "TARGETS"
 726	r.bounds = []string{formatTimestamp(start), formatTimestamp(end)}
 727	r.target = ""
 728	r.doRequest()
 729}
 730
 731func (s *Session) NewHistoryRequest(target string) *HistoryRequest {
 732	return &HistoryRequest{
 733		s:      s,
 734		target: target,
 735		limit:  s.historyLimit,
 736	}
 737}
 738
 739func (s *Session) Whois(nick string) {
 740	s.out <- NewMessage("WHOIS", nick)
 741}
 742
 743func (s *Session) Whowas(nick string) {
 744	s.out <- NewMessage("WHOWAS", nick)
 745}
 746
 747func (s *Session) Invite(nick, channel string) {
 748	s.out <- NewMessage("INVITE", nick, channel)
 749}
 750
 751func (s *Session) Kick(nick, channel, comment string) {
 752	if comment == "" {
 753		s.out <- NewMessage("KICK", channel, nick)
 754	} else {
 755		s.out <- NewMessage("KICK", channel, nick, comment)
 756	}
 757}
 758
 759func (s *Session) HandleMessage(msg Message) (Event, error) {
 760	if msg.Prefix == nil {
 761		if s.defaultPrefix != nil {
 762			msg.Prefix = s.defaultPrefix
 763		} else {
 764			msg.Prefix = &Prefix{
 765				Name: "*",
 766			}
 767		}
 768	}
 769	if s.registered {
 770		return s.handleRegistered(msg)
 771	} else {
 772		return s.handleUnregistered(msg)
 773	}
 774}
 775
 776func (s *Session) handleUnregistered(msg Message) (Event, error) {
 777	switch msg.Command {
 778	case errNicknameinuse:
 779		var nick string
 780		if err := msg.ParseParams(nil, &nick); err != nil {
 781			return nil, err
 782		}
 783
 784		s.out <- NewMessage("NICK", nick+"_")
 785	case rplSaslsuccess:
 786		if s.auth != nil {
 787			s.endRegistration()
 788		}
 789	default:
 790		return s.handleRegistered(msg)
 791	}
 792	return nil, nil
 793}
 794
 795func (s *Session) handleRegistered(msg Message) (Event, error) {
 796	if id, ok := msg.Tags["batch"]; ok {
 797		if id == s.targetsBatchID {
 798			var target, timestamp string
 799			if err := msg.ParseParams(nil, &target, &timestamp); err != nil {
 800				return nil, err
 801			}
 802			t, ok := parseTimestamp(timestamp)
 803			if !ok {
 804				return nil, nil
 805			}
 806			s.targetsBatch.Targets[target] = t
 807		} else if id == s.searchBatchID {
 808			ev, err := s.handleMessageRegistered(msg, true)
 809			if err != nil {
 810				return nil, err
 811			}
 812			if ev, ok := ev.(MessageEvent); ok {
 813				s.searchBatch.Messages = append(s.searchBatch.Messages, ev)
 814				return nil, nil
 815			}
 816		} else if b, ok := s.chBatches[id]; ok {
 817			ev, err := s.handleMessageRegistered(msg, true)
 818			if err != nil {
 819				return nil, err
 820			}
 821			if ev != nil {
 822				s.chBatches[id] = HistoryEvent{
 823					Target:   b.Target,
 824					Messages: append(b.Messages, ev),
 825				}
 826			}
 827			return nil, nil
 828		}
 829	}
 830	return s.handleMessageRegistered(msg, false)
 831}
 832
 833func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, error) {
 834	switch msg.Command {
 835	case "AUTHENTICATE":
 836		if s.auth == nil {
 837			break
 838		}
 839
 840		var payload string
 841		if err := msg.ParseParams(&payload); err != nil {
 842			return nil, err
 843		}
 844
 845		res, err := s.auth.Respond(payload)
 846		if err != nil {
 847			s.out <- NewMessage("AUTHENTICATE", "*")
 848		} else {
 849			s.out <- NewMessage("AUTHENTICATE", res)
 850		}
 851	case rplLoggedin:
 852		var nuh string
 853		if err := msg.ParseParams(nil, &nuh, &s.acct); err != nil {
 854			return nil, err
 855		}
 856
 857		prefix := ParsePrefix(nuh)
 858		s.user = prefix.User
 859		s.host = prefix.Host
 860	case errNicklocked, errSaslfail, errSasltoolong, errSaslaborted, errSaslalready, rplSaslmechs:
 861		if s.auth != nil {
 862			s.endRegistration()
 863		}
 864		return ErrorEvent{
 865			Severity: SeverityFail,
 866			Code:     msg.Command,
 867			Message:  fmt.Sprintf("Registration failed: %s", strings.Join(msg.Params[1:], " ")),
 868		}, nil
 869	case rplWelcome:
 870		s.defaultPrefix = msg.Prefix
 871		if err := msg.ParseParams(&s.nick); err != nil {
 872			return nil, err
 873		}
 874
 875		s.nickCf = s.Casemap(s.nick)
 876		s.registered = true
 877		s.users[s.nickCf] = &User{Name: &Prefix{
 878			Name: s.nick, User: s.user, Host: s.host,
 879		}}
 880		if s.host == "" {
 881			s.Who(s.nick)
 882		}
 883	case rplMyinfo:
 884		if err := msg.ParseParams(nil, nil, &s.serverName); err != nil {
 885			return nil, err
 886		}
 887	case rplIsupport:
 888		if len(msg.Params) < 3 {
 889			return nil, msg.errNotEnoughParams(3)
 890		}
 891		s.updateFeatures(msg.Params[1 : len(msg.Params)-1])
 892		if !s.receivedISupport {
 893			// notify only on first RPL_ISUPPORT
 894			s.receivedISupport = true
 895			return RegisteredEvent{}, nil
 896		}
 897		return nil, nil
 898	case rplWhoreply, rplWhospecialreply:
 899		var nick, host, flags, username string
 900		var err error
 901		if msg.Command == rplWhoreply {
 902			err = msg.ParseParams(nil, nil, &username, &host, nil, &nick, &flags, nil)
 903		} else {
 904			// we always request WHOX with %uhnf
 905			err = msg.ParseParams(nil, &username, &host, &nick, &flags)
 906		}
 907		if err != nil {
 908			return nil, err
 909		}
 910
 911		nickCf := s.Casemap(nick)
 912		away := strings.ContainsRune(flags, 'G')
 913
 914		if s.nickCf == nickCf {
 915			s.user = username
 916			s.host = host
 917		}
 918
 919		if u, ok := s.users[nickCf]; ok {
 920			u.Away = away
 921		}
 922	case rplEndofwho:
 923		// do nothing
 924	case "CAP":
 925		var subcommand, caps string
 926		if err := msg.ParseParams(nil, &subcommand); err != nil {
 927			return nil, err
 928		}
 929		if len(msg.Params) > 3 && msg.Params[2] == "*" {
 930			if err := msg.ParseParams(nil, nil, nil, &caps); err != nil {
 931				return nil, err
 932			}
 933		} else {
 934			if err := msg.ParseParams(nil, nil, &caps); err != nil {
 935				return nil, err
 936			}
 937		}
 938
 939		switch subcommand {
 940		case "ACK":
 941			for _, c := range ParseCaps(caps) {
 942				if c.Enable {
 943					s.enabledCaps[c.Name] = struct{}{}
 944				} else {
 945					delete(s.enabledCaps, c.Name)
 946				}
 947
 948				if s.auth != nil && c.Name == "sasl" {
 949					h := s.auth.Handshake()
 950					s.out <- NewMessage("AUTHENTICATE", h)
 951				} else if len(s.channels) != 0 && c.Name == "multi-prefix" {
 952					// TODO merge NAMES commands
 953					for channel := range s.channels {
 954						s.out <- NewMessage("NAMES", channel)
 955					}
 956				} else if c.Name == "labeled-response" {
 957					if c.Enable {
 958						s.out <- Message{
 959							Command: labelEnableCommand,
 960						}
 961					} else {
 962						s.out <- Message{
 963							Command: labelDisableCommand,
 964						}
 965					}
 966				}
 967			}
 968		case "NAK":
 969			// do nothing
 970		case "LS", "NEW":
 971			var reqs []string
 972			for _, c := range ParseCaps(caps) {
 973				s.availableCaps[c.Name] = c.Value
 974				immediate, ok := SupportedCapabilities[c.Name]
 975				if !ok {
 976					continue
 977				}
 978				if subcommand == "LS" && (immediate || s.netID != "") {
 979					// Already sent CAP, ignore
 980					continue
 981				}
 982				if _, ok := s.enabledCaps[c.Name]; ok {
 983					continue
 984				}
 985				reqs = append(reqs, c.Name)
 986			}
 987			if len(reqs) > 0 {
 988				s.out <- NewMessage("CAP", "REQ", strings.Join(reqs, " "))
 989			}
 990		case "DEL":
 991			for _, c := range ParseCaps(caps) {
 992				delete(s.availableCaps, c.Name)
 993				delete(s.enabledCaps, c.Name)
 994				if c.Name == "labeled-response" {
 995					s.out <- Message{
 996						Command: labelDisableCommand,
 997					}
 998				}
 999			}
1000		}
1001	case "JOIN":
1002		var channel string
1003		if err := msg.ParseParams(&channel); err != nil {
1004			return nil, err
1005		}
1006
1007		if playback {
1008			return UserJoinEvent{
1009				User:    msg.Prefix.Name,
1010				Channel: channel,
1011				Time:    msg.TimeOrNow(),
1012			}, nil
1013		}
1014
1015		nickCf := s.Casemap(msg.Prefix.Name)
1016		channelCf := s.Casemap(channel)
1017
1018		if s.IsMe(nickCf) {
1019			s.channels[channelCf] = Channel{
1020				Name:    msg.Params[0],
1021				Members: map[*User]ChannelMember{},
1022			}
1023			if _, ok := s.enabledCaps["away-notify"]; ok {
1024				// Only try to know who is away if the list is
1025				// updated by the server via away-notify.
1026				// Otherwise, it'll become outdated over time.
1027				s.Who(channel)
1028			}
1029		} else if c, ok := s.channels[channelCf]; ok {
1030			if _, ok := s.users[nickCf]; !ok {
1031				s.users[nickCf] = &User{Name: msg.Prefix.Copy()}
1032			}
1033			c.Members[s.users[nickCf]] = ChannelMember{}
1034			return UserJoinEvent{
1035				User:    msg.Prefix.Name,
1036				Channel: c.Name,
1037				Time:    msg.TimeOrNow(),
1038			}, nil
1039		}
1040	case "PART":
1041		var channel string
1042		var partmessage string
1043		if len(msg.Params) == 2 {
1044			if err := msg.ParseParams(&channel, &partmessage); err != nil {
1045				return nil, err
1046			}
1047		} else {
1048			if err := msg.ParseParams(&channel); err != nil {
1049				return nil, err
1050			}
1051		}
1052
1053		if playback {
1054			return UserPartEvent{
1055				User:    msg.Prefix.Name,
1056				Channel: channel,
1057				Time:    msg.TimeOrNow(),
1058				Message: partmessage,
1059			}, nil
1060		}
1061
1062		nickCf := s.Casemap(msg.Prefix.Name)
1063		channelCf := s.Casemap(channel)
1064
1065		if s.IsMe(nickCf) {
1066			if c, ok := s.channels[channelCf]; ok {
1067				delete(s.channels, channelCf)
1068				for u := range c.Members {
1069					s.cleanUser(u)
1070				}
1071				return SelfPartEvent{
1072					Channel: c.Name,
1073				}, nil
1074			}
1075		} else if c, ok := s.channels[channelCf]; ok {
1076			if u, ok := s.users[nickCf]; ok {
1077				delete(c.Members, u)
1078				s.cleanUser(u)
1079				s.typings.Done(channelCf, nickCf)
1080				return UserPartEvent{
1081					User:    u.Name.Name,
1082					Channel: c.Name,
1083					Time:    msg.TimeOrNow(),
1084				}, nil
1085			}
1086		}
1087	case "KICK":
1088		var channel, nick string
1089		var partmessage string
1090		if len(msg.Params) == 3 {
1091			if err := msg.ParseParams(&channel, &nick, &partmessage); err != nil {
1092				return nil, err
1093			}
1094		} else {
1095			if err := msg.ParseParams(&channel, &nick); err != nil {
1096				return nil, err
1097			}
1098		}
1099
1100		if playback {
1101			return UserPartEvent{
1102				User:    nick,
1103				Channel: channel,
1104				Time:    msg.TimeOrNow(),
1105				Message: partmessage,
1106			}, nil
1107		}
1108
1109		nickCf := s.Casemap(nick)
1110		channelCf := s.Casemap(channel)
1111
1112		if s.IsMe(nickCf) {
1113			if c, ok := s.channels[channelCf]; ok {
1114				delete(s.channels, channelCf)
1115				for u := range c.Members {
1116					s.cleanUser(u)
1117				}
1118				return SelfPartEvent{
1119					Channel: c.Name,
1120				}, nil
1121			}
1122		} else if c, ok := s.channels[channelCf]; ok {
1123			if u, ok := s.users[nickCf]; ok {
1124				delete(c.Members, u)
1125				s.cleanUser(u)
1126				s.typings.Done(channelCf, nickCf)
1127				return UserPartEvent{
1128					User:    nick,
1129					Channel: c.Name,
1130					Time:    msg.TimeOrNow(),
1131					Message: partmessage,
1132					Who:     msg.Prefix.Name,
1133				}, nil
1134			}
1135		}
1136	case "QUIT":
1137		var quitmessage string
1138
1139		if len(msg.Params) == 1 {
1140			if err := msg.ParseParams(&quitmessage); err != nil {
1141				return nil, err
1142			}
1143		}
1144
1145		if playback {
1146			return UserQuitEvent{
1147				User:    msg.Prefix.Name,
1148				Time:    msg.TimeOrNow(),
1149				Message: quitmessage,
1150			}, nil
1151		}
1152
1153		nickCf := s.Casemap(msg.Prefix.Name)
1154
1155		if u, ok := s.users[nickCf]; ok {
1156			u.Disconnected = true
1157			var channels []string
1158			for channelCf, c := range s.channels {
1159				if _, ok := c.Members[u]; ok {
1160					channels = append(channels, c.Name)
1161					delete(c.Members, u)
1162					s.cleanUser(u)
1163					s.typings.Done(channelCf, nickCf)
1164				}
1165			}
1166			return UserQuitEvent{
1167				User:     u.Name.Name,
1168				Channels: channels,
1169				Time:     msg.TimeOrNow(),
1170				Message:  quitmessage,
1171			}, nil
1172		}
1173	case rplMononline:
1174		for _, target := range strings.Split(msg.Params[1], ",") {
1175			prefix := ParsePrefix(target)
1176			if prefix == nil {
1177				continue
1178			}
1179			nickCf := s.casemap(prefix.Name)
1180
1181			if _, ok := s.monitors[nickCf]; ok {
1182				u, ok := s.users[nickCf]
1183				if !ok {
1184					u = &User{
1185						Name: prefix,
1186					}
1187					s.users[nickCf] = u
1188				}
1189				if u.Disconnected {
1190					u.Disconnected = false
1191					return UserOnlineEvent{
1192						User: u.Name.Name,
1193					}, nil
1194				}
1195			}
1196		}
1197	case rplMonoffline:
1198		for _, target := range strings.Split(msg.Params[1], ",") {
1199			prefix := ParsePrefix(target)
1200			if prefix == nil {
1201				continue
1202			}
1203			nickCf := s.casemap(prefix.Name)
1204
1205			if _, ok := s.monitors[nickCf]; ok {
1206				u, ok := s.users[nickCf]
1207				if !ok {
1208					u = &User{
1209						Name: prefix,
1210					}
1211					s.users[nickCf] = u
1212				}
1213				if !u.Disconnected {
1214					u.Disconnected = true
1215					return UserOfflineEvent{
1216						User: u.Name.Name,
1217					}, nil
1218				}
1219			}
1220		}
1221	case rplNamreply:
1222		var channel, names string
1223		if err := msg.ParseParams(nil, nil, &channel, &names); err != nil {
1224			return nil, err
1225		}
1226
1227		channelCf := s.Casemap(channel)
1228
1229		if c, ok := s.channels[channelCf]; ok {
1230
1231			for _, name := range ParseNameReply(names, s.prefixSymbols) {
1232				nickCf := s.Casemap(name.Name.Name)
1233
1234				if _, ok := s.users[nickCf]; !ok {
1235					s.users[nickCf] = &User{Name: name.Name.Copy()}
1236				}
1237				m := c.Members[s.users[nickCf]]
1238				m.Membership = name.PowerLevel
1239				c.Members[s.users[nickCf]] = m
1240			}
1241
1242			s.channels[channelCf] = c
1243		}
1244	case rplEndofnames:
1245		var channel string
1246		if err := msg.ParseParams(nil, &channel); err != nil {
1247			return nil, err
1248		}
1249
1250		channelCf := s.Casemap(channel)
1251
1252		if c, ok := s.channels[channelCf]; ok && !c.complete {
1253			c.complete = true
1254			s.channels[channelCf] = c
1255			ev := SelfJoinEvent{
1256				Channel: c.Name,
1257				Topic:   c.Topic,
1258				Read:    c.Read,
1259			}
1260			if stamp, ok := s.pendingChannels[channelCf]; ok && time.Since(stamp) < 5*time.Second {
1261				ev.Requested = true
1262			}
1263			return ev, nil
1264		}
1265	case rplTopic:
1266		var channel, topic string
1267		if err := msg.ParseParams(nil, &channel, &topic); err != nil {
1268			return nil, err
1269		}
1270
1271		channelCf := s.Casemap(channel)
1272
1273		if c, ok := s.channels[channelCf]; ok {
1274			c.Topic = topic
1275			s.channels[channelCf] = c
1276		}
1277	case rplTopicwhotime:
1278		var channel, topicWho, topicTime string
1279		if err := msg.ParseParams(nil, &channel, &topicWho, &topicTime); err != nil {
1280			return nil, err
1281		}
1282
1283		channelCf := s.Casemap(channel)
1284
1285		// ignore the error, we still have topicWho
1286		t, _ := strconv.ParseInt(topicTime, 10, 64)
1287
1288		if c, ok := s.channels[channelCf]; ok {
1289			c.TopicWho = ParsePrefix(topicWho)
1290			c.TopicTime = time.Unix(t, 0)
1291			s.channels[channelCf] = c
1292		}
1293	case rplNotopic:
1294		var channel string
1295		if err := msg.ParseParams(nil, &channel); err != nil {
1296			return nil, err
1297		}
1298
1299		channelCf := s.Casemap(channel)
1300
1301		if c, ok := s.channels[channelCf]; ok {
1302			c.Topic = ""
1303			s.channels[channelCf] = c
1304		}
1305	case "TOPIC":
1306		var channel, topic string
1307		if err := msg.ParseParams(&channel, &topic); err != nil {
1308			return nil, err
1309		}
1310
1311		if playback {
1312			return TopicChangeEvent{
1313				Channel: channel,
1314				Topic:   topic,
1315				Time:    msg.TimeOrNow(),
1316				Who:     msg.Prefix.Name,
1317			}, nil
1318		}
1319
1320		channelCf := s.Casemap(channel)
1321
1322		if c, ok := s.channels[channelCf]; ok {
1323			c.Topic = topic
1324			c.TopicWho = msg.Prefix.Copy()
1325			c.TopicTime = msg.TimeOrNow()
1326			s.channels[channelCf] = c
1327			return TopicChangeEvent{
1328				Channel: c.Name,
1329				Topic:   c.Topic,
1330				Time:    msg.TimeOrNow(),
1331				Who:     msg.Prefix.Name,
1332			}, nil
1333		}
1334	case "MODE":
1335		var channel string
1336		if err := msg.ParseParams(&channel, nil); err != nil {
1337			return nil, err
1338		}
1339		mode := strings.Join(msg.Params[1:], " ")
1340
1341		if playback {
1342			return ModeChangeEvent{
1343				Channel: channel,
1344				Mode:    mode,
1345				Time:    msg.TimeOrNow(),
1346				Who:     msg.Prefix.Name,
1347			}, nil
1348		}
1349
1350		channelCf := s.Casemap(channel)
1351
1352		if c, ok := s.channels[channelCf]; ok {
1353			modeChanges, err := ParseChannelMode(msg.Params[1], msg.Params[2:], s.chanmodes, s.prefixModes)
1354			if err != nil {
1355				return nil, err
1356			}
1357			for _, change := range modeChanges {
1358				i := strings.IndexByte(s.prefixModes, change.Mode)
1359				if i < 0 {
1360					continue
1361				}
1362				nickCf := s.Casemap(change.Param)
1363				user := s.users[nickCf]
1364				m, ok := c.Members[user]
1365				if !ok {
1366					continue
1367				}
1368				var newMembership []byte
1369				if change.Enable {
1370					newMembership = append([]byte(m.Membership), s.prefixSymbols[i])
1371					sort.Slice(newMembership, func(i, j int) bool {
1372						i = strings.IndexByte(s.prefixSymbols, newMembership[i])
1373						j = strings.IndexByte(s.prefixSymbols, newMembership[j])
1374						return i < j
1375					})
1376				} else if j := strings.IndexByte(m.Membership, s.prefixSymbols[i]); j >= 0 {
1377					newMembership = []byte(m.Membership)
1378					newMembership = append(newMembership[:j], newMembership[j+1:]...)
1379				}
1380				m.Membership = string(newMembership)
1381				c.Members[user] = m
1382			}
1383			s.channels[channelCf] = c
1384			return ModeChangeEvent{
1385				Channel: c.Name,
1386				Mode:    mode,
1387				Time:    msg.TimeOrNow(),
1388				Who:     msg.Prefix.Name,
1389			}, nil
1390		}
1391	case "INVITE":
1392		var nick, channel string
1393		if err := msg.ParseParams(&nick, &channel); err != nil {
1394			return nil, err
1395		}
1396
1397		return InviteEvent{
1398			Inviter: msg.Prefix.Name,
1399			Invitee: nick,
1400			Channel: channel,
1401		}, nil
1402	case rplInviting:
1403		var nick, channel string
1404		if err := msg.ParseParams(nil, &nick, &channel); err != nil {
1405			return nil, err
1406		}
1407
1408		return InviteEvent{
1409			Inviter: s.nick,
1410			Invitee: nick,
1411			Channel: channel,
1412		}, nil
1413	case "AWAY":
1414		nickCf := s.Casemap(msg.Prefix.Name)
1415
1416		if u, ok := s.users[nickCf]; ok {
1417			u.Away = len(msg.Params) == 1
1418		}
1419	case "PRIVMSG", "NOTICE":
1420		if !s.registered && msg.Command == "NOTICE" {
1421			return nil, nil
1422		}
1423
1424		var target string
1425		if err := msg.ParseParams(&target); err != nil {
1426			return nil, err
1427		}
1428
1429		targetCf := strings.TrimLeft(s.casemap(target), s.statusmsg)
1430		nickCf := s.casemap(msg.Prefix.Name)
1431		if !playback {
1432			s.typings.Done(targetCf, nickCf)
1433		}
1434		ev, err := s.newMessageEvent(msg)
1435		if err != nil {
1436			return nil, err
1437		}
1438		if c, ok := s.channels[targetCf]; ok {
1439			if u, ok := s.users[nickCf]; ok {
1440				if m, ok := c.Members[u]; ok {
1441					if ev.Time.After(m.LastActive) {
1442						m.LastActive = ev.Time
1443						c.Members[u] = m
1444					}
1445				}
1446			}
1447		}
1448		return ev, nil
1449	case "TAGMSG":
1450		var target string
1451		if err := msg.ParseParams(&target); err != nil {
1452			return nil, err
1453		}
1454
1455		targetCf := s.casemap(target)
1456		nickCf := s.casemap(msg.Prefix.Name)
1457
1458		// Ignore +typing from self
1459		if t, ok := msg.Tags["+typing"]; ok && !playback && !s.IsMe(msg.Prefix.Name) {
1460			switch t {
1461			case "active":
1462				s.typings.Active(targetCf, nickCf)
1463			case "paused", "done":
1464				s.typings.Done(targetCf, nickCf)
1465			}
1466		}
1467
1468		if id := msg.ReplyTo(); id != "" {
1469			if c, ok := s.channels[targetCf]; ok {
1470				target = c.Name
1471			} else if s.IsMe(target) {
1472				target = msg.Prefix.Name
1473			}
1474			if react, ok := msg.Tags["+draft/react"]; ok {
1475				return ReactEvent{ID: id, User: msg.Prefix.Name, Target: target, React: react}, nil
1476			}
1477			if react, ok := msg.Tags["+draft/unreact"]; ok {
1478				return ReactEvent{ID: id, User: msg.Prefix.Name, Target: target, React: react, Removal: true}, nil
1479			}
1480		}
1481	case "BATCH":
1482		var id string
1483		if err := msg.ParseParams(&id); err != nil {
1484			return nil, err
1485		}
1486		if len(id) == 0 {
1487			return nil, fmt.Errorf("empty batch id")
1488		}
1489
1490		batchStart := id[0] == '+'
1491		id = id[1:]
1492
1493		if batchStart {
1494			var name string
1495			if err := msg.ParseParams(nil, &name); err != nil {
1496				return nil, err
1497			}
1498
1499			switch name {
1500			case "chathistory":
1501				var target string
1502				if err := msg.ParseParams(nil, nil, &target); err != nil {
1503					return nil, err
1504				}
1505
1506				_, end := msg.Tags["draft/chathistory-end"]
1507				s.chBatches[id] = HistoryEvent{Target: target, End: end}
1508			case "draft/chathistory-targets":
1509				s.targetsBatchID = id
1510				s.targetsBatch = HistoryTargetsEvent{Targets: make(map[string]time.Time)}
1511			case "soju.im/search":
1512				s.searchBatchID = id
1513				s.searchBatch = SearchEvent{}
1514			}
1515		} else {
1516			if b, ok := s.chBatches[id]; ok {
1517				delete(s.chBatches, id)
1518				delete(s.chReqs, s.Casemap(b.Target))
1519				return b, nil
1520			} else if s.targetsBatchID == id {
1521				s.targetsBatchID = ""
1522				delete(s.chReqs, "")
1523				return s.targetsBatch, nil
1524			} else if s.searchBatchID == id {
1525				s.searchBatchID = ""
1526				return s.searchBatch, nil
1527			}
1528		}
1529	case "NICK":
1530		var nick string
1531		if err := msg.ParseParams(&nick); err != nil {
1532			return nil, err
1533		}
1534
1535		if playback {
1536			return UserNickEvent{
1537				User:       nick,
1538				FormerNick: msg.Prefix.Name,
1539				Time:       msg.TimeOrNow(),
1540			}, nil
1541		}
1542
1543		nickCf := s.Casemap(msg.Prefix.Name)
1544		newNick := nick
1545		newNickCf := s.Casemap(newNick)
1546
1547		if formerUser, ok := s.users[nickCf]; ok {
1548			formerUser.Name.Name = newNick
1549			delete(s.users, nickCf)
1550			s.users[newNickCf] = formerUser
1551		} else {
1552			break
1553		}
1554
1555		if s.IsMe(msg.Prefix.Name) {
1556			s.nick = newNick
1557			s.nickCf = newNickCf
1558			return SelfNickEvent{
1559				FormerNick: msg.Prefix.Name,
1560			}, nil
1561		} else {
1562			return UserNickEvent{
1563				User:       nick,
1564				FormerNick: msg.Prefix.Name,
1565				Time:       msg.TimeOrNow(),
1566			}, nil
1567		}
1568	case "MARKREAD":
1569		if len(msg.Params) < 2 {
1570			break
1571		}
1572		var target, timestamp string
1573		if err := msg.ParseParams(&target, &timestamp); err != nil {
1574			return nil, err
1575		}
1576		if !strings.HasPrefix(timestamp, "timestamp=") {
1577			return nil, nil
1578		}
1579		timestamp = strings.TrimPrefix(timestamp, "timestamp=")
1580		t, ok := parseTimestamp(timestamp)
1581		if !ok {
1582			return nil, nil
1583		}
1584
1585		channelCf := s.Casemap(target)
1586		if c, ok := s.channels[channelCf]; ok {
1587			c.Read = t
1588			s.channels[channelCf] = c
1589			if !c.complete {
1590				return nil, nil
1591			}
1592		}
1593
1594		return ReadEvent{
1595			Target:    target,
1596			Timestamp: t,
1597		}, nil
1598	case "METADATA":
1599		// METADATA <Target> <Key> <Visibility> <Value>
1600		if len(msg.Params) < 4 {
1601			break
1602		}
1603		var target, key, value string
1604		if err := msg.ParseParams(&target, &key, nil, &value); err != nil {
1605			return nil, err
1606		}
1607		targetCf := s.Casemap(target)
1608		m := s.metadata[targetCf]
1609		switch key {
1610		case "soju.im/pinned":
1611			m.Pinned = value == "1"
1612		case "soju.im/muted":
1613			m.Muted = value == "1"
1614		}
1615		s.metadata[targetCf] = m
1616		ev := MetadataChangeEvent{
1617			Target: target,
1618			Pinned: m.Pinned,
1619			Muted:  m.Muted,
1620		}
1621		return ev, nil
1622	case "BOUNCER":
1623		if len(msg.Params) < 3 {
1624			break
1625		}
1626		if msg.Params[0] != "NETWORK" || s.netID != "" {
1627			break
1628		}
1629		id := msg.Params[1]
1630		event := BouncerNetworkEvent{
1631			ID: id,
1632		}
1633		if msg.Params[2] != "*" {
1634			attrs := parseTags(msg.Params[2])
1635			event.Name = attrs["name"]
1636		} else {
1637			event.Delete = true
1638		}
1639		return event, nil
1640	case "PING":
1641		var payload string
1642		if err := msg.ParseParams(&payload); err != nil {
1643			return nil, err
1644		}
1645
1646		s.out <- NewMessage("PONG", payload)
1647	case "ERROR":
1648		s.Close()
1649	case "FAIL", "WARN", "NOTE":
1650		var severity Severity
1651		var code string
1652		if err := msg.ParseParams(nil, &code); err != nil {
1653			return nil, err
1654		}
1655
1656		switch code {
1657		case "KEY_INVALID": // METADATA SUB failed: ignore
1658			return nil, nil
1659		}
1660
1661		switch msg.Command {
1662		case "FAIL":
1663			severity = SeverityFail
1664		case "WARN":
1665			severity = SeverityWarn
1666		case "NOTE":
1667			severity = SeverityNote
1668		}
1669
1670		return ErrorEvent{
1671			Severity: severity,
1672			Code:     code,
1673			Message:  strings.Join(msg.Params[2:], " "),
1674		}, nil
1675	case errMonlistisfull:
1676		// silence monlist full error, we don't care because we do it best-effort
1677	case rplAway:
1678		// we display user away status, we don't care about automatic AWAY replies
1679	case rplYourhost, rplCreated:
1680		// useless conection messages
1681	case rplAdminme:
1682		// useless admin info header
1683	case rplMotdstart, rplEndofmotd, errNomotd:
1684		// useless motd related messages
1685	case rplHostHidden:
1686		// useless host message
1687	case rplEndofstats:
1688		// useless stats delimiter
1689	case rplEndofwhois:
1690		// useless whois delimiter
1691	case rplListstart:
1692		// useless list delimiter
1693	case rplEndofinvitelist, rplEndofinvexlist:
1694		// useless invite list delimiter
1695	case rplEndofexceptlist:
1696		// useless exception list delimiter
1697	case rplEndoflinks:
1698		// useless links delimiter
1699	case rplEndofbanlist:
1700		// useless ban list delimiter
1701	case rplEndofwhowas:
1702		// useless whois delimiter
1703	case rplEndofinfo:
1704		// useless info delimiter
1705	case rplEndofhelp:
1706		// useless help delimiter
1707	case rplWhoiskeyvalue, rplKeyvalue, rplKeynotset, rplMetadataunsubok, rplMetadatasubs, rplMetadatasynclater:
1708		// useless metadata replies
1709	case rplStatscommands:
1710		var command, count string
1711		if err := msg.ParseParams(nil, &command, &count); err != nil {
1712			return nil, err
1713		}
1714		return InfoEvent{
1715			Prefix:  "Stats",
1716			Message: fmt.Sprintf("The command %s was used %s times on the server", command, count),
1717		}, nil
1718	case rplUmodeis:
1719		if !s.receivedUserMode {
1720			// ignore the first RPL_UMODEIS on join
1721			s.receivedUserMode = true
1722			return nil, nil
1723		}
1724		return InfoEvent{
1725			Message: fmt.Sprintf("The current user modes are: %s", strings.Join(msg.Params[1:], " ")),
1726		}, nil
1727	case rplStatsuptime:
1728		return InfoEvent{
1729			Prefix:  "Stats",
1730			Message: fmt.Sprintf("The server current uptime is: %s", msg.Params[len(msg.Params)-1]),
1731		}, nil
1732	case rplStatsconn, rplLuserclient:
1733		return InfoEvent{
1734			Prefix:  "Stats",
1735			Message: msg.Params[len(msg.Params)-1],
1736		}, nil
1737	case rplLuserop:
1738		var ops string
1739		if err := msg.ParseParams(nil, &ops); err != nil {
1740			return nil, err
1741		}
1742		return InfoEvent{
1743			Prefix:  "Stats",
1744			Message: fmt.Sprintf("There are %s operators online", ops),
1745		}, nil
1746	case rplLuserunknown:
1747		var connections string
1748		if err := msg.ParseParams(nil, &connections); err != nil {
1749			return nil, err
1750		}
1751		return InfoEvent{
1752			Prefix:  "Stats",
1753			Message: fmt.Sprintf("There are %s unknown user connections", connections),
1754		}, nil
1755	case rplLuserchannels:
1756		var channels string
1757		if err := msg.ParseParams(nil, &channels); err != nil {
1758			return nil, err
1759		}
1760		return InfoEvent{
1761			Prefix:  "Stats",
1762			Message: fmt.Sprintf("There are %s channels on the server", channels),
1763		}, nil
1764	case rplLuserme:
1765		return InfoEvent{
1766			Prefix:  "Stats",
1767			Message: fmt.Sprintf("The server current stats are: %s", msg.Params[len(msg.Params)-1]),
1768		}, nil
1769	case rplAdminloc1:
1770		return InfoEvent{
1771			Prefix:  "Admin",
1772			Message: fmt.Sprintf("The server location/environment information is: %s", msg.Params[len(msg.Params)-1]),
1773		}, nil
1774	case rplAdminloc2:
1775		return InfoEvent{
1776			Prefix:  "Admin",
1777			Message: fmt.Sprintf("The server organization information is: %s", msg.Params[len(msg.Params)-1]),
1778		}, nil
1779	case rplAdminemail:
1780		return InfoEvent{
1781			Prefix:  "Admin",
1782			Message: fmt.Sprintf("The server email contact is: %s", msg.Params[len(msg.Params)-1]),
1783		}, nil
1784	case rplLocalusers:
1785		if len(msg.Params) >= 4 {
1786			var currentUsers, maxUsers string
1787			if err := msg.ParseParams(nil, &currentUsers, &maxUsers); err != nil {
1788				return nil, err
1789			}
1790			return InfoEvent{
1791				Prefix:  "Stats",
1792				Message: fmt.Sprintf("There are %s online users on this server, out of a maximum of %s users", currentUsers, maxUsers),
1793			}, nil
1794		} else {
1795			return InfoEvent{
1796				Prefix:  "Stats",
1797				Message: fmt.Sprintf("The server current local user counts are: %s", msg.Params[len(msg.Params)-1]),
1798			}, nil
1799		}
1800	case rplGlobalusers:
1801		if len(msg.Params) >= 4 {
1802			var currentUsers, maxUsers string
1803			if err := msg.ParseParams(nil, &currentUsers, &maxUsers); err != nil {
1804				return nil, err
1805			}
1806			return InfoEvent{
1807				Prefix:  "Stats",
1808				Message: fmt.Sprintf("There are %s online users on the network, out of a maximum of %s users", currentUsers, maxUsers),
1809			}, nil
1810		} else {
1811			return InfoEvent{
1812				Prefix:  "Stats",
1813				Message: fmt.Sprintf("The server current global user counts are: %s", msg.Params[len(msg.Params)-1]),
1814			}, nil
1815		}
1816	case rplWhoiscertfp:
1817		var nick, text string
1818		if err := msg.ParseParams(nil, &nick, &text); err != nil {
1819			return nil, err
1820		}
1821		return InfoEvent{
1822			Prefix:  "User",
1823			Message: fmt.Sprintf("%s %s", nick, text),
1824		}, nil
1825	case rplUnaway:
1826		return InfoEvent{
1827			Message: "You are now marked as back from being away",
1828		}, nil
1829	case rplNowaway:
1830		return InfoEvent{
1831			Message: "You are now marked as away",
1832		}, nil
1833	case rplWhoisregnick:
1834		var nick string
1835		if err := msg.ParseParams(nil, &nick); err != nil {
1836			return nil, err
1837		}
1838		return InfoEvent{
1839			Prefix:  "User",
1840			Message: fmt.Sprintf("%s has identified and is registered to the server", nick),
1841		}, nil
1842	case rplWhoisuser:
1843		var nick, username, host, realname string
1844		if err := msg.ParseParams(nil, &nick, &username, &host, nil, &realname); err != nil {
1845			return nil, err
1846		}
1847		return InfoEvent{
1848			Prefix:  "User",
1849			Message: fmt.Sprintf("%s has username %s and host %s (mask %s!%s@%s); their realname is %s", nick, username, host, nick, username, host, realname),
1850		}, nil
1851	case rplWhoisserver:
1852		var nick, server, serverInfo string
1853		if err := msg.ParseParams(nil, &nick, &server, &serverInfo); err != nil {
1854			return nil, err
1855		}
1856		return InfoEvent{
1857			Prefix:  "User",
1858			Message: fmt.Sprintf("%s is connected through the server %s (%s)", nick, server, serverInfo),
1859		}, nil
1860	case rplWhoisoperator:
1861		var nick, opertype string
1862		if err := msg.ParseParams(nil, &nick, &opertype); err != nil {
1863			return nil, err
1864		}
1865		return InfoEvent{
1866			Prefix:  "User",
1867			Message: fmt.Sprintf("%s %s", nick, opertype),
1868		}, nil
1869	case rplWhowasuser:
1870		var nick, username, host, realname string
1871		if err := msg.ParseParams(nil, &nick, &username, &host, nil, &realname); err != nil {
1872			return nil, err
1873		}
1874		return InfoEvent{
1875			Prefix:  "User",
1876			Message: fmt.Sprintf("%s was last seen with username %s and host %s (mask %s!%s@%s); their realname was %s", nick, username, host, nick, username, host, realname),
1877		}, nil
1878	case rplWhoisidle:
1879		var nick, idleText, signonText string
1880		if err := msg.ParseParams(nil, &nick, &idleText, &signonText); err != nil {
1881			return nil, err
1882		}
1883		idleSeconds, err := strconv.ParseInt(idleText, 10, 64)
1884		if err != nil {
1885			return nil, err
1886		}
1887		signon, err := strconv.ParseInt(signonText, 10, 64)
1888		if err != nil {
1889			return nil, err
1890		}
1891		idle := (time.Duration(idleSeconds) * time.Second).String()
1892		t := time.Unix(signon, 0)
1893		text := fmt.Sprintf("%s was idle for %s; they signed-on on %s", nick, idle, t.Local().Format("January 2 at 15:04"))
1894		return InfoEvent{
1895			Prefix:  "User",
1896			Message: text,
1897		}, nil
1898	case rplWhoischannels:
1899		var nick, text string
1900		if err := msg.ParseParams(nil, &nick, &text); err != nil {
1901			return nil, err
1902		}
1903		return InfoEvent{
1904			Prefix:  "User",
1905			Message: fmt.Sprintf("%s has joined channels: %s", nick, text),
1906		}, nil
1907	case rplWhoisspecial:
1908		var nick, text string
1909		if err := msg.ParseParams(nil, &nick, &text); err != nil {
1910			return nil, err
1911		}
1912		return InfoEvent{
1913			Prefix:  "User",
1914			Message: fmt.Sprintf("%s is also: %s", nick, text),
1915		}, nil
1916	case rplList:
1917		var channel, count, topic string
1918		if err := msg.ParseParams(nil, &channel, &count, &topic); err != nil {
1919			return nil, err
1920		}
1921		s.pendingList = append(s.pendingList, ListItem{
1922			Channel: channel,
1923			Count:   count,
1924			Topic:   topic,
1925		})
1926		return nil, nil
1927	case rplListend:
1928		list := s.pendingList
1929		s.pendingList = nil
1930		return list, nil
1931	case rplChannelmodeis:
1932		var channel string
1933		if err := msg.ParseParams(nil, &channel); err != nil {
1934			return nil, err
1935		}
1936		text := fmt.Sprintf("%s has modes %s", channel, strings.Join(msg.Params[2:], " "))
1937		return InfoEvent{
1938			Message: text,
1939		}, nil
1940	case rplCreationTime:
1941		var channel, creationTime string
1942		if err := msg.ParseParams(nil, &channel, &creationTime); err != nil {
1943			return nil, err
1944		}
1945		creation, err := strconv.ParseInt(creationTime, 10, 64)
1946		if err != nil {
1947			return nil, err
1948		}
1949		t := time.Unix(creation, 0)
1950		text := fmt.Sprintf("%s was created on %s", channel, t.Local().Format("January 2, 2006"))
1951		return InfoEvent{
1952			Message: text,
1953		}, nil
1954	case rplWhoisaccount:
1955		var nick, account string
1956		if err := msg.ParseParams(nil, &nick, &account); err != nil {
1957			return nil, err
1958		}
1959		if nick != account {
1960			return InfoEvent{
1961				Prefix:  "User",
1962				Message: fmt.Sprintf("%s is authenticated as %s", nick, account),
1963			}, nil
1964		} else {
1965			return InfoEvent{
1966				Prefix:  "User",
1967				Message: fmt.Sprintf("%s is authenticated", nick),
1968			}, nil
1969		}
1970	case rplInvitelist, rplInvexlist:
1971		if len(msg.Params) == 2 { // RPL_INVITELIST
1972			var channel string
1973			if err := msg.ParseParams(nil, &channel); err != nil {
1974				return nil, err
1975			}
1976			return InfoEvent{
1977				Prefix:  "Invite",
1978				Message: fmt.Sprintf("You were previously invited to the channel %s", channel),
1979			}, nil
1980		} else { // RPL_INVEXLIST
1981			var channel, mask string
1982			if err := msg.ParseParams(nil, &channel, &mask); err != nil {
1983				return nil, err
1984			}
1985			return InfoEvent{
1986				Prefix:  "Invite-free",
1987				Message: fmt.Sprintf("The channel %s can be joined without invites from host %s", channel, mask),
1988			}, nil
1989		}
1990	case rplWhoisactually:
1991		if len(msg.Params) == 3 {
1992			var nick, text string
1993			if err := msg.ParseParams(nil, &nick, &text); err != nil {
1994				return nil, err
1995			}
1996			return InfoEvent{
1997				Prefix:  "User",
1998				Message: fmt.Sprintf("%s %s", nick, text),
1999			}, nil
2000		} else if len(msg.Params) >= 4 {
2001			var nick string
2002			if err := msg.ParseParams(nil, &nick); err != nil {
2003				return nil, err
2004			}
2005			return InfoEvent{
2006				Prefix:  "User",
2007				Message: fmt.Sprintf("%s is actually using the host %s", nick, msg.Params[len(msg.Params)-2]),
2008			}, nil
2009		}
2010	case rplExceptlist:
2011		var channel, mask string
2012		if err := msg.ParseParams(nil, &channel, &mask); err != nil {
2013			return nil, err
2014		}
2015		return InfoEvent{
2016			Prefix:  "Exempt",
2017			Message: fmt.Sprintf("The channel %s exempts from bans users from host %s", channel, mask),
2018		}, nil
2019	case rplVersion:
2020		var version string
2021		if err := msg.ParseParams(nil, &version); err != nil {
2022			return nil, err
2023		}
2024		return InfoEvent{
2025			Message: fmt.Sprintf("The server is running: %s", version),
2026		}, nil
2027	case rplLinks:
2028		var prefix, last string
2029		if err := msg.ParseParams(nil, &prefix, nil, &last); err != nil {
2030			return nil, err
2031		}
2032		hop, info, ok := strings.Cut(last, " ")
2033		if !ok {
2034			hop = "0"
2035			info = last
2036		}
2037		var count int
2038		if c, err := strconv.Atoi(hop); err == nil {
2039			count = c
2040		}
2041		return InfoEvent{
2042			Prefix:  "Link",
2043			Message: fmt.Sprintf("The network has server %s%s (%s)", strings.Repeat("* ", count), prefix, info),
2044		}, nil
2045	case rplBanlist:
2046		if len(msg.Params) >= 5 {
2047			var channel, mask, who, whenText string
2048			if err := msg.ParseParams(nil, &channel, &mask, &who, &whenText); err != nil {
2049				return nil, err
2050			}
2051			when, err := strconv.ParseInt(whenText, 10, 64)
2052			if err != nil {
2053				return nil, err
2054			}
2055			t := time.Unix(when, 0).Local().Format("January 2 2006 at 15:04")
2056			return InfoEvent{
2057				Prefix:  "Ban",
2058				Message: fmt.Sprintf("The channel %s has %s banned by %s on %s", channel, mask, who, t),
2059			}, nil
2060		} else {
2061			var channel, mask string
2062			if err := msg.ParseParams(nil, &channel, &mask); err != nil {
2063				return nil, err
2064			}
2065			return InfoEvent{
2066				Prefix:  "Ban",
2067				Message: fmt.Sprintf("The channel %s has %s banned", channel, mask),
2068			}, nil
2069		}
2070	case rplInfo:
2071		var text string
2072		if err := msg.ParseParams(nil, &text); err != nil {
2073			return nil, err
2074		}
2075		return InfoEvent{
2076			Prefix:  "Info",
2077			Message: text,
2078		}, nil
2079	case rplMotd:
2080		return InfoEvent{
2081			Prefix:  "MotD",
2082			Message: msg.Params[1],
2083		}, nil
2084	case rplWhoishost:
2085		var nick, text string
2086		if err := msg.ParseParams(nil, &nick, &text); err != nil {
2087			return nil, err
2088		}
2089		return InfoEvent{
2090			Prefix:  "User",
2091			Message: fmt.Sprintf("%s %s", nick, text),
2092		}, nil
2093	case rplWhoismodes:
2094		var nick, text string
2095		if err := msg.ParseParams(nil, &nick, &text); err != nil {
2096			return nil, err
2097		}
2098		return InfoEvent{
2099			Prefix:  "User",
2100			Message: fmt.Sprintf("%s %s", nick, text),
2101		}, nil
2102	case rplYoureoper:
2103		var text string
2104		if err := msg.ParseParams(nil, &text); err != nil {
2105			return nil, err
2106		}
2107		return InfoEvent{
2108			Message: text,
2109		}, nil
2110	case rplRehashing:
2111		return InfoEvent{
2112			Message: "The server configuration is now reloading (rehash)",
2113		}, nil
2114	case rplTime:
2115		return InfoEvent{
2116			Message: fmt.Sprintf("The server current local time is: %s", msg.Params[len(msg.Params)-1]),
2117		}, nil
2118	case rplWhoissecure:
2119		var nick, text string
2120		if err := msg.ParseParams(nil, &nick, &text); err != nil {
2121			return nil, err
2122		}
2123		return InfoEvent{
2124			Prefix:  "User",
2125			Message: fmt.Sprintf("%s %s", nick, text),
2126		}, nil
2127	case rplMetadatasubok:
2128		if err := msg.ParseParams(nil); err != nil {
2129			return nil, err
2130		}
2131		for _, key := range msg.Params[1:] {
2132			s.metadataSubs[key] = struct{}{}
2133		}
2134		return nil, nil
2135	case rplHelpstart, rplHelptxt:
2136		var text string
2137		if err := msg.ParseParams(nil, nil, &text); err != nil {
2138			return nil, err
2139		}
2140		return InfoEvent{
2141			Prefix:  "Help",
2142			Message: text,
2143		}, nil
2144	default:
2145		if msg.IsReply() {
2146			if len(msg.Params) < 2 {
2147				return nil, msg.errNotEnoughParams(2)
2148			}
2149			if msg.Command == rplUmodeis && !s.receivedUserMode {
2150				// ignore the first RPL_UMODEIS on join
2151				s.receivedUserMode = true
2152				return nil, nil
2153
2154			}
2155			if msg.Command == errUnknowncommand {
2156				switch msg.Params[1] {
2157				case "BOUNCER":
2158					// ignore any error in response to unconditional BOUNCER LISTNETWORKS
2159					return nil, nil
2160				case "METADATA":
2161					// ignore any error in response to unconditional METADATA SUB
2162					return nil, nil
2163				}
2164			}
2165			return ErrorEvent{
2166				Severity: ReplySeverity(msg.Command),
2167				Code:     msg.Command,
2168				Message:  strings.Join(msg.Params[1:], " "),
2169			}, nil
2170		}
2171	}
2172	return nil, nil
2173}
2174
2175func (s *Session) newMessageEvent(msg Message) (ev MessageEvent, err error) {
2176	var rawTarget, content string
2177	if err := msg.ParseParams(&rawTarget, &content); err != nil {
2178		return ev, err
2179	}
2180
2181	i := strings.IndexFunc(rawTarget, func(r rune) bool {
2182		return !strings.ContainsRune(s.statusmsg, r)
2183	})
2184	if i < 0 {
2185		i = len(rawTarget)
2186	}
2187	prefix := rawTarget[:i]
2188	target := rawTarget[i:]
2189
2190	ev = MessageEvent{
2191		ID:           msg.ID(),
2192		ReplyTo:      msg.ReplyTo(),
2193		User:         msg.Prefix.Name, // TODO correctly casemap
2194		Target:       target,          // TODO correctly casemap
2195		TargetPrefix: prefix,
2196		Command:      msg.Command,
2197		Content:      content,
2198		Time:         msg.TimeOrNow(),
2199	}
2200
2201	if s.IsMe(target) {
2202		if context := msg.Tags["+draft/channel-context"]; context != "" {
2203			target = context
2204			ev.TargetPrefix = "*"
2205		}
2206	}
2207	targetCf := s.Casemap(target)
2208	if c, ok := s.channels[targetCf]; ok {
2209		ev.Target = c.Name
2210		ev.TargetIsChannel = true
2211	}
2212
2213	return ev, nil
2214}
2215
2216func (s *Session) cleanUser(parted *User) {
2217	nameCf := s.Casemap(parted.Name.Name)
2218	if _, ok := s.monitors[nameCf]; ok {
2219		return
2220	}
2221	for _, c := range s.channels {
2222		if _, ok := c.Members[parted]; ok {
2223			return
2224		}
2225	}
2226	delete(s.users, nameCf)
2227}
2228
2229func (s *Session) updateFeatures(features []string) {
2230	for _, f := range features {
2231		if f == "" || f == "-" || f == "=" || f == "-=" {
2232			continue
2233		}
2234
2235		var (
2236			add   bool
2237			key   string
2238			value string
2239		)
2240
2241		if strings.HasPrefix(f, "-") {
2242			add = false
2243			f = f[1:]
2244		} else {
2245			add = true
2246		}
2247
2248		kv := strings.SplitN(f, "=", 2)
2249		key = strings.ToUpper(kv[0])
2250		if len(kv) > 1 {
2251			value = kv[1]
2252		}
2253
2254		if !add {
2255			// TODO support ISUPPORT negations
2256			continue
2257		}
2258
2259	Switch:
2260		switch key {
2261		case "BOUNCER_NETID":
2262			s.netID = value
2263		case "CASEMAPPING":
2264			oldNickCf := s.nickCf
2265			switch value {
2266			case "ascii":
2267				s.casemap = CasemapASCII
2268			default:
2269				s.casemap = CasemapRFC1459
2270			}
2271			s.nickCf = s.casemap(s.nick)
2272			if oldNickCf != s.nickCf {
2273				if u, ok := s.users[oldNickCf]; ok {
2274					delete(s.users, oldNickCf)
2275					s.users[s.nickCf] = u
2276				}
2277			}
2278		case "CLIENTTAGDENY":
2279			clear(s.clientTagList)
2280			s.clientTagListIsAllow = strings.HasPrefix(value, "*")
2281			tags := strings.Split(value, ",")
2282			if s.clientTagListIsAllow {
2283				tags = tags[1:]
2284			}
2285			for _, tag := range tags {
2286				if s.clientTagListIsAllow {
2287					if !strings.HasPrefix(tag, "-") {
2288						continue
2289					}
2290					tag = tag[1:]
2291				} else if strings.HasPrefix(tag, "-") {
2292					continue
2293				}
2294				s.clientTagList[tag] = struct{}{}
2295			}
2296		case "CHANMODES":
2297			// We only care about the first four params
2298			types := strings.SplitN(value, ",", 5)
2299			for i := 0; i < len(types) && i < len(s.chanmodes); i++ {
2300				s.chanmodes[i] = types[i]
2301			}
2302		case "CHANTYPES":
2303			s.chantypes = value
2304		case "CHATHISTORY":
2305			historyLimit, err := strconv.Atoi(value)
2306			if err == nil {
2307				s.historyLimit = historyLimit
2308			}
2309		case "ELIST":
2310			s.listMask = strings.Contains(strings.ToUpper(value), "M")
2311		case "LINELEN":
2312			linelen, err := strconv.Atoi(value)
2313			if err == nil && linelen != 0 {
2314				s.linelen = linelen
2315			}
2316		case "MONITOR":
2317			monitor, err := strconv.Atoi(value)
2318			if err == nil && monitor > 0 {
2319				s.monitor = true
2320			}
2321		case "PREFIX":
2322			if value == "" {
2323				s.prefixModes = ""
2324				s.prefixSymbols = ""
2325				break Switch
2326			}
2327			if len(value)%2 != 0 {
2328				break Switch
2329			}
2330			for i := 0; i < len(value); i++ {
2331				if unicode.MaxASCII < value[i] {
2332					break Switch
2333				}
2334			}
2335			numPrefixes := len(value)/2 - 1
2336			s.prefixModes = value[1 : numPrefixes+1]
2337			s.prefixSymbols = value[numPrefixes+2:]
2338		case "STATUSMSG":
2339			s.statusmsg = value
2340		case "WHOX":
2341			s.whox = true
2342		case "SOJU.IM/FILEHOST":
2343			s.upload = value
2344		}
2345	}
2346}
2347
2348func (s *Session) endRegistration() {
2349	if s.registered {
2350		return
2351	}
2352	if len(s.enabledCaps) == 0 || s.HasCapability("draft/metadata-2") {
2353		// Best effort to avoid a round trip: subscribe to metadata if explicitly supported or if CAPs are not yet known
2354		s.out <- NewMessage("METADATA", "*", "SUB", "soju.im/pinned", "soju.im/muted")
2355	}
2356	if s.netID != "" {
2357		s.out <- NewMessage("BOUNCER", "BIND", s.netID)
2358		s.out <- NewMessage("CAP", "END")
2359	} else {
2360		s.out <- NewMessage("CAP", "END")
2361		s.out <- NewMessage("BOUNCER", "LISTNETWORKS")
2362	}
2363}