source: code/trunk/upstream.go@ 200

Last change on this file since 200 was 198, checked in by contact, 5 years ago

Auto away

Closes: https://todo.sr.ht/~emersion/soju/13

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