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, ×tamp); 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, ×tamp); 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, ¤tUsers, &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, ¤tUsers, &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}