source: code/trunk/upstream.go@ 207

Last change on this file since 207 was 206, checked in by contact, 5 years ago

Set connect timeout

References: https://todo.sr.ht/~emersion/soju/26

File size: 37.5 KB
Line 
1package soju
2
3import (
4 "crypto/tls"
5 "encoding/base64"
6 "errors"
7 "fmt"
8 "io"
9 "net"
10 "os"
11 "path/filepath"
12 "strconv"
13 "strings"
14 "time"
15
16 "github.com/emersion/go-sasl"
17 "gopkg.in/irc.v3"
18)
19
20type upstreamChannel struct {
21 Name string
22 conn *upstreamConn
23 Topic string
24 TopicWho string
25 TopicTime time.Time
26 Status channelStatus
27 modes channelModes
28 creationTime string
29 Members map[string]*membership
30 complete bool
31}
32
33type upstreamConn struct {
34 network *network
35 logger Logger
36 net net.Conn
37 irc *irc.Conn
38 srv *Server
39 user *user
40 outgoing chan<- *irc.Message
41 closed chan struct{}
42
43 serverName string
44 availableUserModes string
45 availableChannelModes map[byte]channelModeType
46 availableChannelTypes string
47 availableMemberships []membership
48
49 registered bool
50 nick string
51 username string
52 realname string
53 modes userModes
54 channels map[string]*upstreamChannel
55 caps map[string]string
56 batches map[string]batch
57 away bool
58
59 tagsSupported bool
60 labelsSupported bool
61 nextLabelID uint64
62
63 saslClient sasl.Client
64 saslStarted bool
65
66 // set of LIST commands in progress, per downstream
67 pendingLISTDownstreamSet map[uint64]struct{}
68
69 logs map[string]entityLog
70}
71
72type entityLog struct {
73 name string
74 file *os.File
75}
76
77func connectToUpstream(network *network) (*upstreamConn, error) {
78 logger := &prefixLogger{network.user.srv.Logger, fmt.Sprintf("upstream %q: ", network.Addr)}
79
80 addr := network.Addr
81 if !strings.ContainsRune(addr, ':') {
82 addr = addr + ":6697"
83 }
84
85 dialer := net.Dialer{Timeout: connectTimeout}
86
87 logger.Printf("connecting to TLS server at address %q", addr)
88 netConn, err := tls.DialWithDialer(&dialer, "tcp", addr, nil)
89 if err != nil {
90 return nil, fmt.Errorf("failed to dial %q: %v", addr, err)
91 }
92
93 setKeepAlive(netConn)
94
95 outgoing := make(chan *irc.Message, 64)
96 uc := &upstreamConn{
97 network: network,
98 logger: logger,
99 net: netConn,
100 irc: irc.NewConn(netConn),
101 srv: network.user.srv,
102 user: network.user,
103 outgoing: outgoing,
104 closed: make(chan struct{}),
105 channels: make(map[string]*upstreamChannel),
106 caps: make(map[string]string),
107 batches: make(map[string]batch),
108 availableChannelTypes: stdChannelTypes,
109 availableChannelModes: stdChannelModes,
110 availableMemberships: stdMemberships,
111 pendingLISTDownstreamSet: make(map[uint64]struct{}),
112 logs: make(map[string]entityLog),
113 }
114
115 go func() {
116 // TODO: any SendMessage call after the connection is closed will
117 // either block or drop
118 for {
119 var closed bool
120 select {
121 case msg := <-outgoing:
122 if uc.srv.Debug {
123 uc.logger.Printf("sent: %v", msg)
124 }
125 uc.net.SetWriteDeadline(time.Now().Add(writeTimeout))
126 if err := uc.irc.WriteMessage(msg); err != nil {
127 uc.logger.Printf("failed to write message: %v", err)
128 closed = true
129 }
130 case <-uc.closed:
131 closed = true
132 }
133 if closed {
134 break
135 }
136 }
137 if err := uc.net.Close(); err != nil {
138 uc.logger.Printf("failed to close connection: %v", err)
139 } else {
140 uc.logger.Printf("connection closed")
141 }
142 }()
143
144 return uc, nil
145}
146
147func (uc *upstreamConn) isClosed() bool {
148 select {
149 case <-uc.closed:
150 return true
151 default:
152 return false
153 }
154}
155
156// Close closes the connection. It is safe to call from any goroutine.
157func (uc *upstreamConn) Close() error {
158 if uc.isClosed() {
159 return fmt.Errorf("upstream connection already closed")
160 }
161 close(uc.closed)
162 return nil
163}
164
165func (uc *upstreamConn) forEachDownstream(f func(*downstreamConn)) {
166 uc.user.forEachDownstream(func(dc *downstreamConn) {
167 if dc.network != nil && dc.network != uc.network {
168 return
169 }
170 f(dc)
171 })
172}
173
174func (uc *upstreamConn) forEachDownstreamByID(id uint64, f func(*downstreamConn)) {
175 uc.forEachDownstream(func(dc *downstreamConn) {
176 if id != 0 && id != dc.id {
177 return
178 }
179 f(dc)
180 })
181}
182
183func (uc *upstreamConn) getChannel(name string) (*upstreamChannel, error) {
184 ch, ok := uc.channels[name]
185 if !ok {
186 return nil, fmt.Errorf("unknown channel %q", name)
187 }
188 return ch, nil
189}
190
191func (uc *upstreamConn) isChannel(entity string) bool {
192 if i := strings.IndexByte(uc.availableChannelTypes, entity[0]); i >= 0 {
193 return true
194 }
195 return false
196}
197
198func (uc *upstreamConn) getPendingLIST() *pendingLIST {
199 for _, pl := range uc.user.pendingLISTs {
200 if _, ok := pl.pendingCommands[uc.network.ID]; !ok {
201 continue
202 }
203 return &pl
204 }
205 return nil
206}
207
208func (uc *upstreamConn) endPendingLISTs(all bool) (found bool) {
209 found = false
210 for i := 0; i < len(uc.user.pendingLISTs); i++ {
211 pl := uc.user.pendingLISTs[i]
212 if _, ok := pl.pendingCommands[uc.network.ID]; !ok {
213 continue
214 }
215 delete(pl.pendingCommands, uc.network.ID)
216 if len(pl.pendingCommands) == 0 {
217 uc.user.pendingLISTs = append(uc.user.pendingLISTs[:i], uc.user.pendingLISTs[i+1:]...)
218 i--
219 uc.forEachDownstreamByID(pl.downstreamID, func(dc *downstreamConn) {
220 dc.SendMessage(&irc.Message{
221 Prefix: dc.srv.prefix(),
222 Command: irc.RPL_LISTEND,
223 Params: []string{dc.nick, "End of /LIST"},
224 })
225 })
226 }
227 found = true
228 if !all {
229 delete(uc.pendingLISTDownstreamSet, pl.downstreamID)
230 uc.user.forEachUpstream(func(uc *upstreamConn) {
231 uc.trySendLIST(pl.downstreamID)
232 })
233 return
234 }
235 }
236 return
237}
238
239func (uc *upstreamConn) trySendLIST(downstreamID uint64) {
240 // must be called with a lock in uc.user.pendingLISTsLock
241
242 if _, ok := uc.pendingLISTDownstreamSet[downstreamID]; ok {
243 // a LIST command is already pending
244 // we will try again when that command is completed
245 return
246 }
247
248 for _, pl := range uc.user.pendingLISTs {
249 if pl.downstreamID != downstreamID {
250 continue
251 }
252 // this is the first pending LIST command list of the downstream
253 listCommand, ok := pl.pendingCommands[uc.network.ID]
254 if !ok {
255 // there is no command for this upstream in these LIST commands
256 // do not send anything
257 continue
258 }
259 // there is a command for this upstream in these LIST commands
260 // send it now
261
262 uc.SendMessageLabeled(downstreamID, listCommand)
263
264 uc.pendingLISTDownstreamSet[downstreamID] = struct{}{}
265 return
266 }
267}
268
269func (uc *upstreamConn) parseMembershipPrefix(s string) (membership *membership, nick string) {
270 for _, m := range uc.availableMemberships {
271 if m.Prefix == s[0] {
272 return &m, s[1:]
273 }
274 }
275 return nil, s
276}
277
278func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
279 var label string
280 if l, ok := msg.GetTag("label"); ok {
281 label = l
282 }
283
284 var msgBatch *batch
285 if batchName, ok := msg.GetTag("batch"); ok {
286 b, ok := uc.batches[batchName]
287 if !ok {
288 return fmt.Errorf("unexpected batch reference: batch was not defined: %q", batchName)
289 }
290 msgBatch = &b
291 if label == "" {
292 label = msgBatch.Label
293 }
294 }
295
296 var downstreamID uint64 = 0
297 if label != "" {
298 var labelOffset uint64
299 n, err := fmt.Sscanf(label, "sd-%d-%d", &downstreamID, &labelOffset)
300 if err == nil && n < 2 {
301 err = errors.New("not enough arguments")
302 }
303 if err != nil {
304 return fmt.Errorf("unexpected message label: invalid downstream reference for label %q: %v", label, err)
305 }
306 }
307
308 switch msg.Command {
309 case "PING":
310 uc.SendMessage(&irc.Message{
311 Command: "PONG",
312 Params: msg.Params,
313 })
314 return nil
315 case "NOTICE":
316 uc.logger.Print(msg)
317
318 if msg.Prefix.User == "" && msg.Prefix.Host == "" { // server message
319 uc.forEachDownstream(func(dc *downstreamConn) {
320 dc.SendMessage(&irc.Message{
321 Command: "NOTICE",
322 Params: msg.Params,
323 })
324 })
325 } else { // regular user NOTICE
326 var nick, text string
327 if err := parseMessageParams(msg, &nick, &text); err != nil {
328 return err
329 }
330
331 target := nick
332 if nick == uc.nick {
333 target = msg.Prefix.Name
334 }
335 uc.appendLog(target, "<%s> %s", msg.Prefix.Name, text)
336
337 uc.forEachDownstream(func(dc *downstreamConn) {
338 dc.SendMessage(&irc.Message{
339 Prefix: dc.marshalUserPrefix(uc, msg.Prefix),
340 Command: "NOTICE",
341 Params: []string{dc.marshalEntity(uc, nick), text},
342 })
343 })
344 }
345 case "CAP":
346 var subCmd string
347 if err := parseMessageParams(msg, nil, &subCmd); err != nil {
348 return err
349 }
350 subCmd = strings.ToUpper(subCmd)
351 subParams := msg.Params[2:]
352 switch subCmd {
353 case "LS":
354 if len(subParams) < 1 {
355 return newNeedMoreParamsError(msg.Command)
356 }
357 caps := strings.Fields(subParams[len(subParams)-1])
358 more := len(subParams) >= 2 && msg.Params[len(subParams)-2] == "*"
359
360 for _, s := range caps {
361 kv := strings.SplitN(s, "=", 2)
362 k := strings.ToLower(kv[0])
363 var v string
364 if len(kv) == 2 {
365 v = kv[1]
366 }
367 uc.caps[k] = v
368 }
369
370 if more {
371 break // wait to receive all capabilities
372 }
373
374 requestCaps := make([]string, 0, 16)
375 for _, c := range []string{"message-tags", "batch", "labeled-response", "server-time"} {
376 if _, ok := uc.caps[c]; ok {
377 requestCaps = append(requestCaps, c)
378 }
379 }
380
381 if uc.requestSASL() {
382 requestCaps = append(requestCaps, "sasl")
383 }
384
385 if len(requestCaps) > 0 {
386 uc.SendMessage(&irc.Message{
387 Command: "CAP",
388 Params: []string{"REQ", strings.Join(requestCaps, " ")},
389 })
390 }
391
392 if uc.requestSASL() {
393 break // we'll send CAP END after authentication is completed
394 }
395
396 uc.SendMessage(&irc.Message{
397 Command: "CAP",
398 Params: []string{"END"},
399 })
400 case "ACK", "NAK":
401 if len(subParams) < 1 {
402 return newNeedMoreParamsError(msg.Command)
403 }
404 caps := strings.Fields(subParams[0])
405
406 for _, name := range caps {
407 if err := uc.handleCapAck(strings.ToLower(name), subCmd == "ACK"); err != nil {
408 return err
409 }
410 }
411
412 if uc.saslClient == nil {
413 uc.SendMessage(&irc.Message{
414 Command: "CAP",
415 Params: []string{"END"},
416 })
417 }
418 default:
419 uc.logger.Printf("unhandled message: %v", msg)
420 }
421 case "AUTHENTICATE":
422 if uc.saslClient == nil {
423 return fmt.Errorf("received unexpected AUTHENTICATE message")
424 }
425
426 // TODO: if a challenge is 400 bytes long, buffer it
427 var challengeStr string
428 if err := parseMessageParams(msg, &challengeStr); err != nil {
429 uc.SendMessage(&irc.Message{
430 Command: "AUTHENTICATE",
431 Params: []string{"*"},
432 })
433 return err
434 }
435
436 var challenge []byte
437 if challengeStr != "+" {
438 var err error
439 challenge, err = base64.StdEncoding.DecodeString(challengeStr)
440 if err != nil {
441 uc.SendMessage(&irc.Message{
442 Command: "AUTHENTICATE",
443 Params: []string{"*"},
444 })
445 return err
446 }
447 }
448
449 var resp []byte
450 var err error
451 if !uc.saslStarted {
452 _, resp, err = uc.saslClient.Start()
453 uc.saslStarted = true
454 } else {
455 resp, err = uc.saslClient.Next(challenge)
456 }
457 if err != nil {
458 uc.SendMessage(&irc.Message{
459 Command: "AUTHENTICATE",
460 Params: []string{"*"},
461 })
462 return err
463 }
464
465 // TODO: send response in multiple chunks if >= 400 bytes
466 var respStr = "+"
467 if resp != nil {
468 respStr = base64.StdEncoding.EncodeToString(resp)
469 }
470
471 uc.SendMessage(&irc.Message{
472 Command: "AUTHENTICATE",
473 Params: []string{respStr},
474 })
475 case irc.RPL_LOGGEDIN:
476 var account string
477 if err := parseMessageParams(msg, nil, nil, &account); err != nil {
478 return err
479 }
480 uc.logger.Printf("logged in with account %q", account)
481 case irc.RPL_LOGGEDOUT:
482 uc.logger.Printf("logged out")
483 case irc.ERR_NICKLOCKED, irc.RPL_SASLSUCCESS, irc.ERR_SASLFAIL, irc.ERR_SASLTOOLONG, irc.ERR_SASLABORTED:
484 var info string
485 if err := parseMessageParams(msg, nil, &info); err != nil {
486 return err
487 }
488 switch msg.Command {
489 case irc.ERR_NICKLOCKED:
490 uc.logger.Printf("invalid nick used with SASL authentication: %v", info)
491 case irc.ERR_SASLFAIL:
492 uc.logger.Printf("SASL authentication failed: %v", info)
493 case irc.ERR_SASLTOOLONG:
494 uc.logger.Printf("SASL message too long: %v", info)
495 }
496
497 uc.saslClient = nil
498 uc.saslStarted = false
499
500 uc.SendMessage(&irc.Message{
501 Command: "CAP",
502 Params: []string{"END"},
503 })
504 case irc.RPL_WELCOME:
505 uc.registered = true
506 uc.logger.Printf("connection registered")
507
508 channels, err := uc.srv.db.ListChannels(uc.network.ID)
509 if err != nil {
510 uc.logger.Printf("failed to list channels from database: %v", err)
511 break
512 }
513
514 for _, ch := range channels {
515 params := []string{ch.Name}
516 if ch.Key != "" {
517 params = append(params, ch.Key)
518 }
519 uc.SendMessage(&irc.Message{
520 Command: "JOIN",
521 Params: params,
522 })
523 }
524 case irc.RPL_MYINFO:
525 if err := parseMessageParams(msg, nil, &uc.serverName, nil, &uc.availableUserModes, nil); err != nil {
526 return err
527 }
528 case irc.RPL_ISUPPORT:
529 if err := parseMessageParams(msg, nil, nil); err != nil {
530 return err
531 }
532 for _, token := range msg.Params[1 : len(msg.Params)-1] {
533 negate := false
534 parameter := token
535 value := ""
536 if strings.HasPrefix(token, "-") {
537 negate = true
538 token = token[1:]
539 } else {
540 if i := strings.IndexByte(token, '='); i >= 0 {
541 parameter = token[:i]
542 value = token[i+1:]
543 }
544 }
545 if !negate {
546 switch parameter {
547 case "CHANMODES":
548 parts := strings.SplitN(value, ",", 5)
549 if len(parts) < 4 {
550 return fmt.Errorf("malformed ISUPPORT CHANMODES value: %v", value)
551 }
552 modes := make(map[byte]channelModeType)
553 for i, mt := range []channelModeType{modeTypeA, modeTypeB, modeTypeC, modeTypeD} {
554 for j := 0; j < len(parts[i]); j++ {
555 mode := parts[i][j]
556 modes[mode] = mt
557 }
558 }
559 uc.availableChannelModes = modes
560 case "CHANTYPES":
561 uc.availableChannelTypes = value
562 case "PREFIX":
563 if value == "" {
564 uc.availableMemberships = nil
565 } else {
566 if value[0] != '(' {
567 return fmt.Errorf("malformed ISUPPORT PREFIX value: %v", value)
568 }
569 sep := strings.IndexByte(value, ')')
570 if sep < 0 || len(value) != sep*2 {
571 return fmt.Errorf("malformed ISUPPORT PREFIX value: %v", value)
572 }
573 memberships := make([]membership, len(value)/2-1)
574 for i := range memberships {
575 memberships[i] = membership{
576 Mode: value[i+1],
577 Prefix: value[sep+i+1],
578 }
579 }
580 uc.availableMemberships = memberships
581 }
582 }
583 } else {
584 // TODO: handle ISUPPORT negations
585 }
586 }
587 case "BATCH":
588 var tag string
589 if err := parseMessageParams(msg, &tag); err != nil {
590 return err
591 }
592
593 if strings.HasPrefix(tag, "+") {
594 tag = tag[1:]
595 if _, ok := uc.batches[tag]; ok {
596 return fmt.Errorf("unexpected BATCH reference tag: batch was already defined: %q", tag)
597 }
598 var batchType string
599 if err := parseMessageParams(msg, nil, &batchType); err != nil {
600 return err
601 }
602 label := label
603 if label == "" && msgBatch != nil {
604 label = msgBatch.Label
605 }
606 uc.batches[tag] = batch{
607 Type: batchType,
608 Params: msg.Params[2:],
609 Outer: msgBatch,
610 Label: label,
611 }
612 } else if strings.HasPrefix(tag, "-") {
613 tag = tag[1:]
614 if _, ok := uc.batches[tag]; !ok {
615 return fmt.Errorf("unknown BATCH reference tag: %q", tag)
616 }
617 delete(uc.batches, tag)
618 } else {
619 return fmt.Errorf("unexpected BATCH reference tag: missing +/- prefix: %q", tag)
620 }
621 case "NICK":
622 if msg.Prefix == nil {
623 return fmt.Errorf("expected a prefix")
624 }
625
626 var newNick string
627 if err := parseMessageParams(msg, &newNick); err != nil {
628 return err
629 }
630
631 if msg.Prefix.Name == uc.nick {
632 uc.logger.Printf("changed nick from %q to %q", uc.nick, newNick)
633 uc.nick = newNick
634 }
635
636 for _, ch := range uc.channels {
637 if membership, ok := ch.Members[msg.Prefix.Name]; ok {
638 delete(ch.Members, msg.Prefix.Name)
639 ch.Members[newNick] = membership
640 uc.appendLog(ch.Name, "*** %s is now known as %s", msg.Prefix.Name, newNick)
641 }
642 }
643
644 if msg.Prefix.Name != uc.nick {
645 uc.forEachDownstream(func(dc *downstreamConn) {
646 dc.SendMessage(&irc.Message{
647 Prefix: dc.marshalUserPrefix(uc, msg.Prefix),
648 Command: "NICK",
649 Params: []string{newNick},
650 })
651 })
652 }
653 case "JOIN":
654 if msg.Prefix == nil {
655 return fmt.Errorf("expected a prefix")
656 }
657
658 var channels string
659 if err := parseMessageParams(msg, &channels); err != nil {
660 return err
661 }
662
663 for _, ch := range strings.Split(channels, ",") {
664 if msg.Prefix.Name == uc.nick {
665 uc.logger.Printf("joined channel %q", ch)
666 uc.channels[ch] = &upstreamChannel{
667 Name: ch,
668 conn: uc,
669 Members: make(map[string]*membership),
670 }
671
672 uc.SendMessage(&irc.Message{
673 Command: "MODE",
674 Params: []string{ch},
675 })
676 } else {
677 ch, err := uc.getChannel(ch)
678 if err != nil {
679 return err
680 }
681 ch.Members[msg.Prefix.Name] = nil
682 }
683
684 uc.appendLog(ch, "*** Joins: %s (%s@%s)", msg.Prefix.Name, msg.Prefix.User, msg.Prefix.Host)
685
686 uc.forEachDownstream(func(dc *downstreamConn) {
687 dc.SendMessage(&irc.Message{
688 Prefix: dc.marshalUserPrefix(uc, msg.Prefix),
689 Command: "JOIN",
690 Params: []string{dc.marshalChannel(uc, ch)},
691 })
692 })
693 }
694 case "PART":
695 if msg.Prefix == nil {
696 return fmt.Errorf("expected a prefix")
697 }
698
699 var channels string
700 if err := parseMessageParams(msg, &channels); err != nil {
701 return err
702 }
703
704 var reason string
705 if len(msg.Params) > 1 {
706 reason = msg.Params[1]
707 }
708
709 for _, ch := range strings.Split(channels, ",") {
710 if msg.Prefix.Name == uc.nick {
711 uc.logger.Printf("parted channel %q", ch)
712 delete(uc.channels, ch)
713 } else {
714 ch, err := uc.getChannel(ch)
715 if err != nil {
716 return err
717 }
718 delete(ch.Members, msg.Prefix.Name)
719 }
720
721 uc.appendLog(ch, "*** Parts: %s (%s@%s) (%s)", msg.Prefix.Name, msg.Prefix.User, msg.Prefix.Host, reason)
722
723 uc.forEachDownstream(func(dc *downstreamConn) {
724 dc.SendMessage(&irc.Message{
725 Prefix: dc.marshalUserPrefix(uc, msg.Prefix),
726 Command: "PART",
727 Params: []string{dc.marshalChannel(uc, ch)},
728 })
729 })
730 }
731 case "KICK":
732 if msg.Prefix == nil {
733 return fmt.Errorf("expected a prefix")
734 }
735
736 var channel, user string
737 if err := parseMessageParams(msg, &channel, &user); err != nil {
738 return err
739 }
740
741 var reason string
742 if len(msg.Params) > 2 {
743 reason = msg.Params[1]
744 }
745
746 if user == uc.nick {
747 uc.logger.Printf("kicked from channel %q by %s", channel, msg.Prefix.Name)
748 delete(uc.channels, channel)
749 } else {
750 ch, err := uc.getChannel(channel)
751 if err != nil {
752 return err
753 }
754 delete(ch.Members, user)
755 }
756
757 uc.appendLog(channel, "*** %s was kicked by %s (%s)", user, msg.Prefix.Name, reason)
758
759 uc.forEachDownstream(func(dc *downstreamConn) {
760 params := []string{dc.marshalChannel(uc, channel), dc.marshalNick(uc, user)}
761 if reason != "" {
762 params = append(params, reason)
763 }
764 dc.SendMessage(&irc.Message{
765 Prefix: dc.marshalUserPrefix(uc, msg.Prefix),
766 Command: "KICK",
767 Params: params,
768 })
769 })
770 case "QUIT":
771 if msg.Prefix == nil {
772 return fmt.Errorf("expected a prefix")
773 }
774
775 var reason string
776 if len(msg.Params) > 0 {
777 reason = msg.Params[0]
778 }
779
780 if msg.Prefix.Name == uc.nick {
781 uc.logger.Printf("quit")
782 }
783
784 for _, ch := range uc.channels {
785 if _, ok := ch.Members[msg.Prefix.Name]; ok {
786 delete(ch.Members, msg.Prefix.Name)
787
788 uc.appendLog(ch.Name, "*** Quits: %s (%s@%s) (%s)", msg.Prefix.Name, msg.Prefix.User, msg.Prefix.Host, reason)
789 }
790 }
791
792 if msg.Prefix.Name != uc.nick {
793 uc.forEachDownstream(func(dc *downstreamConn) {
794 dc.SendMessage(&irc.Message{
795 Prefix: dc.marshalUserPrefix(uc, msg.Prefix),
796 Command: "QUIT",
797 Params: msg.Params,
798 })
799 })
800 }
801 case irc.RPL_TOPIC, irc.RPL_NOTOPIC:
802 var name, topic string
803 if err := parseMessageParams(msg, nil, &name, &topic); err != nil {
804 return err
805 }
806 ch, err := uc.getChannel(name)
807 if err != nil {
808 return err
809 }
810 if msg.Command == irc.RPL_TOPIC {
811 ch.Topic = topic
812 } else {
813 ch.Topic = ""
814 }
815 case "TOPIC":
816 var name string
817 if err := parseMessageParams(msg, &name); err != nil {
818 return err
819 }
820 ch, err := uc.getChannel(name)
821 if err != nil {
822 return err
823 }
824 if len(msg.Params) > 1 {
825 ch.Topic = msg.Params[1]
826 } else {
827 ch.Topic = ""
828 }
829 uc.forEachDownstream(func(dc *downstreamConn) {
830 params := []string{dc.marshalChannel(uc, name)}
831 if ch.Topic != "" {
832 params = append(params, ch.Topic)
833 }
834 dc.SendMessage(&irc.Message{
835 Prefix: dc.marshalUserPrefix(uc, msg.Prefix),
836 Command: "TOPIC",
837 Params: params,
838 })
839 })
840 case "MODE":
841 var name, modeStr string
842 if err := parseMessageParams(msg, &name, &modeStr); err != nil {
843 return err
844 }
845
846 if !uc.isChannel(name) { // user mode change
847 if name != uc.nick {
848 return fmt.Errorf("received MODE message for unknown nick %q", name)
849 }
850 return uc.modes.Apply(modeStr)
851 // TODO: notify downstreams about user mode change?
852 } else { // channel mode change
853 ch, err := uc.getChannel(name)
854 if err != nil {
855 return err
856 }
857
858 if ch.modes != nil {
859 if err := ch.modes.Apply(uc.availableChannelModes, modeStr, msg.Params[2:]...); err != nil {
860 return err
861 }
862 }
863
864 modeMsg := modeStr
865 for _, v := range msg.Params[2:] {
866 modeMsg += " " + v
867 }
868 uc.appendLog(ch.Name, "*** %s sets mode: %s", msg.Prefix.Name, modeMsg)
869
870 uc.forEachDownstream(func(dc *downstreamConn) {
871 params := []string{dc.marshalChannel(uc, name), modeStr}
872 params = append(params, msg.Params[2:]...)
873
874 dc.SendMessage(&irc.Message{
875 Prefix: dc.marshalUserPrefix(uc, msg.Prefix),
876 Command: "MODE",
877 Params: params,
878 })
879 })
880 }
881 case irc.RPL_UMODEIS:
882 if err := parseMessageParams(msg, nil); err != nil {
883 return err
884 }
885 modeStr := ""
886 if len(msg.Params) > 1 {
887 modeStr = msg.Params[1]
888 }
889
890 uc.modes = ""
891 if err := uc.modes.Apply(modeStr); err != nil {
892 return err
893 }
894 // TODO: send RPL_UMODEIS to downstream connections when applicable
895 case irc.RPL_CHANNELMODEIS:
896 var channel string
897 if err := parseMessageParams(msg, nil, &channel); err != nil {
898 return err
899 }
900 modeStr := ""
901 if len(msg.Params) > 2 {
902 modeStr = msg.Params[2]
903 }
904
905 ch, err := uc.getChannel(channel)
906 if err != nil {
907 return err
908 }
909
910 firstMode := ch.modes == nil
911 ch.modes = make(map[byte]string)
912 if err := ch.modes.Apply(uc.availableChannelModes, modeStr, msg.Params[3:]...); err != nil {
913 return err
914 }
915 if firstMode {
916 modeStr, modeParams := ch.modes.Format()
917
918 uc.forEachDownstream(func(dc *downstreamConn) {
919 params := []string{dc.nick, dc.marshalChannel(uc, channel), modeStr}
920 params = append(params, modeParams...)
921
922 dc.SendMessage(&irc.Message{
923 Prefix: dc.srv.prefix(),
924 Command: irc.RPL_CHANNELMODEIS,
925 Params: params,
926 })
927 })
928 }
929 case rpl_creationtime:
930 var channel, creationTime string
931 if err := parseMessageParams(msg, nil, &channel, &creationTime); err != nil {
932 return err
933 }
934
935 ch, err := uc.getChannel(channel)
936 if err != nil {
937 return err
938 }
939
940 firstCreationTime := ch.creationTime == ""
941 ch.creationTime = creationTime
942 if firstCreationTime {
943 uc.forEachDownstream(func(dc *downstreamConn) {
944 dc.SendMessage(&irc.Message{
945 Prefix: dc.srv.prefix(),
946 Command: rpl_creationtime,
947 Params: []string{dc.nick, channel, creationTime},
948 })
949 })
950 }
951 case rpl_topicwhotime:
952 var name, who, timeStr string
953 if err := parseMessageParams(msg, nil, &name, &who, &timeStr); err != nil {
954 return err
955 }
956 ch, err := uc.getChannel(name)
957 if err != nil {
958 return err
959 }
960 ch.TopicWho = who
961 sec, err := strconv.ParseInt(timeStr, 10, 64)
962 if err != nil {
963 return fmt.Errorf("failed to parse topic time: %v", err)
964 }
965 ch.TopicTime = time.Unix(sec, 0)
966 case irc.RPL_LIST:
967 var channel, clients, topic string
968 if err := parseMessageParams(msg, nil, &channel, &clients, &topic); err != nil {
969 return err
970 }
971
972 pl := uc.getPendingLIST()
973 if pl == nil {
974 return fmt.Errorf("unexpected RPL_LIST: no matching pending LIST")
975 }
976
977 uc.forEachDownstreamByID(pl.downstreamID, func(dc *downstreamConn) {
978 dc.SendMessage(&irc.Message{
979 Prefix: dc.srv.prefix(),
980 Command: irc.RPL_LIST,
981 Params: []string{dc.nick, dc.marshalChannel(uc, channel), clients, topic},
982 })
983 })
984 case irc.RPL_LISTEND:
985 ok := uc.endPendingLISTs(false)
986 if !ok {
987 return fmt.Errorf("unexpected RPL_LISTEND: no matching pending LIST")
988 }
989 case irc.RPL_NAMREPLY:
990 var name, statusStr, members string
991 if err := parseMessageParams(msg, nil, &statusStr, &name, &members); err != nil {
992 return err
993 }
994
995 ch, ok := uc.channels[name]
996 if !ok {
997 // NAMES on a channel we have not joined, forward to downstream
998 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
999 channel := dc.marshalChannel(uc, name)
1000 members := splitSpace(members)
1001 for i, member := range members {
1002 membership, nick := uc.parseMembershipPrefix(member)
1003 members[i] = membership.String() + dc.marshalNick(uc, nick)
1004 }
1005 memberStr := strings.Join(members, " ")
1006
1007 dc.SendMessage(&irc.Message{
1008 Prefix: dc.srv.prefix(),
1009 Command: irc.RPL_NAMREPLY,
1010 Params: []string{dc.nick, statusStr, channel, memberStr},
1011 })
1012 })
1013 return nil
1014 }
1015
1016 status, err := parseChannelStatus(statusStr)
1017 if err != nil {
1018 return err
1019 }
1020 ch.Status = status
1021
1022 for _, s := range splitSpace(members) {
1023 membership, nick := uc.parseMembershipPrefix(s)
1024 ch.Members[nick] = membership
1025 }
1026 case irc.RPL_ENDOFNAMES:
1027 var name string
1028 if err := parseMessageParams(msg, nil, &name); err != nil {
1029 return err
1030 }
1031
1032 ch, ok := uc.channels[name]
1033 if !ok {
1034 // NAMES on a channel we have not joined, forward to downstream
1035 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
1036 channel := dc.marshalChannel(uc, name)
1037
1038 dc.SendMessage(&irc.Message{
1039 Prefix: dc.srv.prefix(),
1040 Command: irc.RPL_ENDOFNAMES,
1041 Params: []string{dc.nick, channel, "End of /NAMES list"},
1042 })
1043 })
1044 return nil
1045 }
1046
1047 if ch.complete {
1048 return fmt.Errorf("received unexpected RPL_ENDOFNAMES")
1049 }
1050 ch.complete = true
1051
1052 uc.forEachDownstream(func(dc *downstreamConn) {
1053 forwardChannel(dc, ch)
1054 })
1055 case irc.RPL_WHOREPLY:
1056 var channel, username, host, server, nick, mode, trailing string
1057 if err := parseMessageParams(msg, nil, &channel, &username, &host, &server, &nick, &mode, &trailing); err != nil {
1058 return err
1059 }
1060
1061 parts := strings.SplitN(trailing, " ", 2)
1062 if len(parts) != 2 {
1063 return fmt.Errorf("received malformed RPL_WHOREPLY: wrong trailing parameter: %s", trailing)
1064 }
1065 realname := parts[1]
1066 hops, err := strconv.Atoi(parts[0])
1067 if err != nil {
1068 return fmt.Errorf("received malformed RPL_WHOREPLY: wrong hop count: %s", parts[0])
1069 }
1070 hops++
1071
1072 trailing = strconv.Itoa(hops) + " " + realname
1073
1074 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
1075 channel := channel
1076 if channel != "*" {
1077 channel = dc.marshalChannel(uc, channel)
1078 }
1079 nick := dc.marshalNick(uc, nick)
1080 dc.SendMessage(&irc.Message{
1081 Prefix: dc.srv.prefix(),
1082 Command: irc.RPL_WHOREPLY,
1083 Params: []string{dc.nick, channel, username, host, server, nick, mode, trailing},
1084 })
1085 })
1086 case irc.RPL_ENDOFWHO:
1087 var name string
1088 if err := parseMessageParams(msg, nil, &name); err != nil {
1089 return err
1090 }
1091
1092 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
1093 name := name
1094 if name != "*" {
1095 // TODO: support WHO masks
1096 name = dc.marshalEntity(uc, name)
1097 }
1098 dc.SendMessage(&irc.Message{
1099 Prefix: dc.srv.prefix(),
1100 Command: irc.RPL_ENDOFWHO,
1101 Params: []string{dc.nick, name, "End of /WHO list"},
1102 })
1103 })
1104 case irc.RPL_WHOISUSER:
1105 var nick, username, host, realname string
1106 if err := parseMessageParams(msg, nil, &nick, &username, &host, nil, &realname); err != nil {
1107 return err
1108 }
1109
1110 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
1111 nick := dc.marshalNick(uc, nick)
1112 dc.SendMessage(&irc.Message{
1113 Prefix: dc.srv.prefix(),
1114 Command: irc.RPL_WHOISUSER,
1115 Params: []string{dc.nick, nick, username, host, "*", realname},
1116 })
1117 })
1118 case irc.RPL_WHOISSERVER:
1119 var nick, server, serverInfo string
1120 if err := parseMessageParams(msg, nil, &nick, &server, &serverInfo); err != nil {
1121 return err
1122 }
1123
1124 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
1125 nick := dc.marshalNick(uc, nick)
1126 dc.SendMessage(&irc.Message{
1127 Prefix: dc.srv.prefix(),
1128 Command: irc.RPL_WHOISSERVER,
1129 Params: []string{dc.nick, nick, server, serverInfo},
1130 })
1131 })
1132 case irc.RPL_WHOISOPERATOR:
1133 var nick string
1134 if err := parseMessageParams(msg, nil, &nick); err != nil {
1135 return err
1136 }
1137
1138 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
1139 nick := dc.marshalNick(uc, nick)
1140 dc.SendMessage(&irc.Message{
1141 Prefix: dc.srv.prefix(),
1142 Command: irc.RPL_WHOISOPERATOR,
1143 Params: []string{dc.nick, nick, "is an IRC operator"},
1144 })
1145 })
1146 case irc.RPL_WHOISIDLE:
1147 var nick string
1148 if err := parseMessageParams(msg, nil, &nick, nil); err != nil {
1149 return err
1150 }
1151
1152 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
1153 nick := dc.marshalNick(uc, nick)
1154 params := []string{dc.nick, nick}
1155 params = append(params, msg.Params[2:]...)
1156 dc.SendMessage(&irc.Message{
1157 Prefix: dc.srv.prefix(),
1158 Command: irc.RPL_WHOISIDLE,
1159 Params: params,
1160 })
1161 })
1162 case irc.RPL_WHOISCHANNELS:
1163 var nick, channelList string
1164 if err := parseMessageParams(msg, nil, &nick, &channelList); err != nil {
1165 return err
1166 }
1167 channels := splitSpace(channelList)
1168
1169 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
1170 nick := dc.marshalNick(uc, nick)
1171 channelList := make([]string, len(channels))
1172 for i, channel := range channels {
1173 prefix, channel := uc.parseMembershipPrefix(channel)
1174 channel = dc.marshalChannel(uc, channel)
1175 channelList[i] = prefix.String() + channel
1176 }
1177 channels := strings.Join(channelList, " ")
1178 dc.SendMessage(&irc.Message{
1179 Prefix: dc.srv.prefix(),
1180 Command: irc.RPL_WHOISCHANNELS,
1181 Params: []string{dc.nick, nick, channels},
1182 })
1183 })
1184 case irc.RPL_ENDOFWHOIS:
1185 var nick string
1186 if err := parseMessageParams(msg, nil, &nick); err != nil {
1187 return err
1188 }
1189
1190 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
1191 nick := dc.marshalNick(uc, nick)
1192 dc.SendMessage(&irc.Message{
1193 Prefix: dc.srv.prefix(),
1194 Command: irc.RPL_ENDOFWHOIS,
1195 Params: []string{dc.nick, nick, "End of /WHOIS list"},
1196 })
1197 })
1198 case "PRIVMSG":
1199 if msg.Prefix == nil {
1200 return fmt.Errorf("expected a prefix")
1201 }
1202
1203 var nick, text string
1204 if err := parseMessageParams(msg, &nick, &text); err != nil {
1205 return err
1206 }
1207
1208 if msg.Prefix.Name == serviceNick {
1209 uc.logger.Printf("skipping PRIVMSG from soju's service: %v", msg)
1210 break
1211 }
1212 if nick == serviceNick {
1213 uc.logger.Printf("skipping PRIVMSG to soju's service: %v", msg)
1214 break
1215 }
1216
1217 if _, ok := msg.Tags["time"]; !ok {
1218 msg.Tags["time"] = irc.TagValue(time.Now().Format(serverTimeLayout))
1219 }
1220
1221 target := nick
1222 if nick == uc.nick {
1223 target = msg.Prefix.Name
1224 }
1225 uc.appendLog(target, "<%s> %s", msg.Prefix.Name, text)
1226
1227 uc.network.ring.Produce(msg)
1228 case "INVITE":
1229 var nick string
1230 var channel string
1231 if err := parseMessageParams(msg, &nick, &channel); err != nil {
1232 return err
1233 }
1234
1235 uc.forEachDownstream(func(dc *downstreamConn) {
1236 dc.SendMessage(&irc.Message{
1237 Prefix: dc.marshalUserPrefix(uc, msg.Prefix),
1238 Command: "INVITE",
1239 Params: []string{dc.marshalNick(uc, nick), dc.marshalChannel(uc, channel)},
1240 })
1241 })
1242 case irc.RPL_INVITING:
1243 var nick string
1244 var channel string
1245 if err := parseMessageParams(msg, &nick, &channel); err != nil {
1246 return err
1247 }
1248
1249 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
1250 dc.SendMessage(&irc.Message{
1251 Prefix: dc.srv.prefix(),
1252 Command: irc.RPL_INVITING,
1253 Params: []string{dc.nick, dc.marshalNick(uc, nick), dc.marshalChannel(uc, channel)},
1254 })
1255 })
1256 case irc.ERR_UNKNOWNCOMMAND, irc.RPL_TRYAGAIN:
1257 var command, reason string
1258 if err := parseMessageParams(msg, nil, &command, &reason); err != nil {
1259 return err
1260 }
1261
1262 if command == "LIST" {
1263 ok := uc.endPendingLISTs(false)
1264 if !ok {
1265 return fmt.Errorf("unexpected response for LIST: %q: no matching pending LIST", msg.Command)
1266 }
1267 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
1268 dc.SendMessage(&irc.Message{
1269 Prefix: uc.srv.prefix(),
1270 Command: msg.Command,
1271 Params: []string{dc.nick, "LIST", reason},
1272 })
1273 })
1274 }
1275 case "TAGMSG":
1276 // TODO: relay to downstream connections that accept message-tags
1277 case "ACK":
1278 // Ignore
1279 case irc.RPL_NOWAWAY, irc.RPL_UNAWAY:
1280 // Ignore
1281 case irc.RPL_YOURHOST, irc.RPL_CREATED:
1282 // Ignore
1283 case irc.RPL_LUSERCLIENT, irc.RPL_LUSEROP, irc.RPL_LUSERUNKNOWN, irc.RPL_LUSERCHANNELS, irc.RPL_LUSERME:
1284 // Ignore
1285 case irc.RPL_MOTDSTART, irc.RPL_MOTD, irc.RPL_ENDOFMOTD:
1286 // Ignore
1287 case irc.RPL_LISTSTART:
1288 // Ignore
1289 case rpl_localusers, rpl_globalusers:
1290 // Ignore
1291 case irc.RPL_STATSVLINE, rpl_statsping, irc.RPL_STATSBLINE, irc.RPL_STATSDLINE:
1292 // Ignore
1293 default:
1294 uc.logger.Printf("unhandled message: %v", msg)
1295 }
1296 return nil
1297}
1298
1299func splitSpace(s string) []string {
1300 return strings.FieldsFunc(s, func(r rune) bool {
1301 return r == ' '
1302 })
1303}
1304
1305func (uc *upstreamConn) register() {
1306 uc.nick = uc.network.Nick
1307 uc.username = uc.network.Username
1308 if uc.username == "" {
1309 uc.username = uc.nick
1310 }
1311 uc.realname = uc.network.Realname
1312 if uc.realname == "" {
1313 uc.realname = uc.nick
1314 }
1315
1316 uc.SendMessage(&irc.Message{
1317 Command: "CAP",
1318 Params: []string{"LS", "302"},
1319 })
1320
1321 if uc.network.Pass != "" {
1322 uc.SendMessage(&irc.Message{
1323 Command: "PASS",
1324 Params: []string{uc.network.Pass},
1325 })
1326 }
1327
1328 uc.SendMessage(&irc.Message{
1329 Command: "NICK",
1330 Params: []string{uc.nick},
1331 })
1332 uc.SendMessage(&irc.Message{
1333 Command: "USER",
1334 Params: []string{uc.username, "0", "*", uc.realname},
1335 })
1336}
1337
1338func (uc *upstreamConn) runUntilRegistered() error {
1339 for !uc.registered {
1340 msg, err := uc.irc.ReadMessage()
1341 if err != nil {
1342 return fmt.Errorf("failed to read message: %v", err)
1343 }
1344
1345 if uc.srv.Debug {
1346 uc.logger.Printf("received: %v", msg)
1347 }
1348
1349 if err := uc.handleMessage(msg); err != nil {
1350 return fmt.Errorf("failed to handle message %q: %v", msg, err)
1351 }
1352 }
1353
1354 return nil
1355}
1356
1357func (uc *upstreamConn) requestSASL() bool {
1358 if uc.network.SASL.Mechanism == "" {
1359 return false
1360 }
1361
1362 v, ok := uc.caps["sasl"]
1363 if !ok {
1364 return false
1365 }
1366 if v != "" {
1367 mechanisms := strings.Split(v, ",")
1368 found := false
1369 for _, mech := range mechanisms {
1370 if strings.EqualFold(mech, uc.network.SASL.Mechanism) {
1371 found = true
1372 break
1373 }
1374 }
1375 if !found {
1376 return false
1377 }
1378 }
1379
1380 return true
1381}
1382
1383func (uc *upstreamConn) handleCapAck(name string, ok bool) error {
1384 auth := &uc.network.SASL
1385 switch name {
1386 case "sasl":
1387 if !ok {
1388 uc.logger.Printf("server refused to acknowledge the SASL capability")
1389 return nil
1390 }
1391
1392 switch auth.Mechanism {
1393 case "PLAIN":
1394 uc.logger.Printf("starting SASL PLAIN authentication with username %q", auth.Plain.Username)
1395 uc.saslClient = sasl.NewPlainClient("", auth.Plain.Username, auth.Plain.Password)
1396 default:
1397 return fmt.Errorf("unsupported SASL mechanism %q", name)
1398 }
1399
1400 uc.SendMessage(&irc.Message{
1401 Command: "AUTHENTICATE",
1402 Params: []string{auth.Mechanism},
1403 })
1404 case "message-tags":
1405 uc.tagsSupported = ok
1406 case "labeled-response":
1407 uc.labelsSupported = ok
1408 case "batch", "server-time":
1409 // Nothing to do
1410 default:
1411 uc.logger.Printf("received CAP ACK/NAK for a cap we don't support: %v", name)
1412 }
1413 return nil
1414}
1415
1416func (uc *upstreamConn) readMessages(ch chan<- event) error {
1417 for {
1418 msg, err := uc.irc.ReadMessage()
1419 if err == io.EOF {
1420 break
1421 } else if err != nil {
1422 return fmt.Errorf("failed to read IRC command: %v", err)
1423 }
1424
1425 if uc.srv.Debug {
1426 uc.logger.Printf("received: %v", msg)
1427 }
1428
1429 ch <- eventUpstreamMessage{msg, uc}
1430 }
1431
1432 return nil
1433}
1434
1435// SendMessage queues a new outgoing message. It is safe to call from any
1436// goroutine.
1437func (uc *upstreamConn) SendMessage(msg *irc.Message) {
1438 uc.outgoing <- msg
1439}
1440
1441func (uc *upstreamConn) SendMessageLabeled(downstreamID uint64, msg *irc.Message) {
1442 if uc.labelsSupported {
1443 if msg.Tags == nil {
1444 msg.Tags = make(map[string]irc.TagValue)
1445 }
1446 msg.Tags["label"] = irc.TagValue(fmt.Sprintf("sd-%d-%d", downstreamID, uc.nextLabelID))
1447 uc.nextLabelID++
1448 }
1449 uc.SendMessage(msg)
1450}
1451
1452// TODO: handle moving logs when a network name changes, when support for this is added
1453func (uc *upstreamConn) appendLog(entity string, format string, a ...interface{}) {
1454 if uc.srv.LogPath == "" {
1455 return
1456 }
1457 // TODO: enforce maximum open file handles (LRU cache of file handles)
1458 // TODO: handle non-monotonic clock behaviour
1459 now := time.Now()
1460 year, month, day := now.Date()
1461 name := fmt.Sprintf("%04d-%02d-%02d.log", year, month, day)
1462 log, ok := uc.logs[entity]
1463 if !ok || log.name != name {
1464 if ok {
1465 log.file.Close()
1466 delete(uc.logs, entity)
1467 }
1468 // TODO: handle/forbid network/entity names with illegal path characters
1469 dir := filepath.Join(uc.srv.LogPath, uc.user.Username, uc.network.Name, entity)
1470 if err := os.MkdirAll(dir, 0700); err != nil {
1471 uc.logger.Printf("failed to log message: could not create logs directory %q: %v", dir, err)
1472 return
1473 }
1474 path := filepath.Join(dir, name)
1475 f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)
1476 if err != nil {
1477 uc.logger.Printf("failed to log message: could not open or create log file %q: %v", path, err)
1478 return
1479 }
1480 log = entityLog{
1481 name: name,
1482 file: f,
1483 }
1484 uc.logs[entity] = log
1485 }
1486
1487 format = "[%02d:%02d:%02d] " + format + "\n"
1488 args := []interface{}{now.Hour(), now.Minute(), now.Second()}
1489 args = append(args, a...)
1490
1491 if _, err := fmt.Fprintf(log.file, format, args...); err != nil {
1492 uc.logger.Printf("failed to log message to %q: %v", log.name, err)
1493 }
1494}
1495
1496func (uc *upstreamConn) updateAway() {
1497 away := true
1498 uc.forEachDownstream(func(*downstreamConn) {
1499 away = false
1500 })
1501 if away == uc.away {
1502 return
1503 }
1504 if away {
1505 uc.SendMessage(&irc.Message{
1506 Command: "AWAY",
1507 Params: []string{"Auto away"},
1508 })
1509 } else {
1510 uc.SendMessage(&irc.Message{
1511 Command: "AWAY",
1512 })
1513 }
1514 uc.away = away
1515}
Note: See TracBrowser for help on using the repository browser.