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