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