Browse Source

Merge

tags/v0.11.0-beta
Daniel Oaks 6 years ago
parent
commit
3680a3fe9a
7 changed files with 265 additions and 46 deletions
  1. 78
    0
      irc/batch.go
  2. 33
    24
      irc/client.go
  3. 7
    2
      irc/commands.go
  4. 12
    7
      irc/handlers.go
  5. 1
    1
      irc/modes.go
  6. 120
    0
      irc/responsebuffer.go
  7. 14
    12
      irc/server.go

+ 78
- 0
irc/batch.go View File

1
+// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"strconv"
8
+	"time"
9
+
10
+	"github.com/goshuirc/irc-go/ircmsg"
11
+	"github.com/oragono/oragono/irc/caps"
12
+)
13
+
14
+const (
15
+	// maxBatchID is the maximum ID the batch counter can get to before it rotates.
16
+	//
17
+	// Batch IDs are made up of the current unix timestamp plus a rolling int ID that's
18
+	// incremented for every new batch. It's an alright solution and will work unless we get
19
+	// more than maxId batches per nanosecond. Later on when we have S2S linking, the batch
20
+	// ID will also contain the server ID to ensure they stay unique.
21
+	maxBatchID uint64 = 60000
22
+)
23
+
24
+// BatchManager helps generate new batches and new batch IDs.
25
+type BatchManager struct {
26
+	idCounter uint64
27
+}
28
+
29
+// NewBatchManager returns a new Manager.
30
+func NewBatchManager() *BatchManager {
31
+	return &BatchManager{}
32
+}
33
+
34
+// NewID returns a new batch ID that should be unique.
35
+func (bm *BatchManager) NewID() string {
36
+	bm.idCounter++
37
+	if maxBatchID < bm.idCounter {
38
+		bm.idCounter = 0
39
+	}
40
+
41
+	return strconv.FormatInt(time.Now().UnixNano(), 36) + strconv.FormatUint(bm.idCounter, 36)
42
+}
43
+
44
+// Batch represents an IRCv3 batch.
45
+type Batch struct {
46
+	ID     string
47
+	Type   string
48
+	Params []string
49
+}
50
+
51
+// New returns a new batch.
52
+func (bm *BatchManager) New(batchType string, params ...string) *Batch {
53
+	newBatch := Batch{
54
+		ID:     bm.NewID(),
55
+		Type:   batchType,
56
+		Params: params,
57
+	}
58
+
59
+	return &newBatch
60
+}
61
+
62
+// Start sends the batch start message to this client
63
+func (b *Batch) Start(client *Client, tags *map[string]ircmsg.TagValue) {
64
+	if client.capabilities.Has(caps.Batch) {
65
+		params := []string{"+" + b.ID, b.Type}
66
+		for _, param := range b.Params {
67
+			params = append(params, param)
68
+		}
69
+		client.Send(tags, client.server.name, "BATCH", params...)
70
+	}
71
+}
72
+
73
+// End sends the batch end message to this client
74
+func (b *Batch) End(client *Client) {
75
+	if client.capabilities.Has(caps.Batch) {
76
+		client.Send(nil, client.server.name, "BATCH", "-"+b.ID)
77
+	}
78
+}

+ 33
- 24
irc/client.go View File

744
 
744
 
745
 var (
745
 var (
746
 	// these are all the output commands that MUST have their last param be a trailing.
746
 	// these are all the output commands that MUST have their last param be a trailing.
747
-	// this is needed because silly clients like to treat trailing as separate from the
747
+	// this is needed because dumb clients like to treat trailing params separately from the
748
 	// other params in messages.
748
 	// other params in messages.
749
 	commandsThatMustUseTrailing = map[string]bool{
749
 	commandsThatMustUseTrailing = map[string]bool{
750
 		"PRIVMSG": true,
750
 		"PRIVMSG": true,
755
 	}
755
 	}
756
 )
756
 )
757
 
757
 
758
-// Send sends an IRC line to the client.
759
-func (client *Client) Send(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) error {
760
-	// attach server-time
761
-	if client.capabilities.Has(caps.ServerTime) {
762
-		t := time.Now().UTC().Format("2006-01-02T15:04:05.999Z")
763
-		if tags == nil {
764
-			tags = ircmsg.MakeTags("time", t)
765
-		} else {
766
-			(*tags)["time"] = ircmsg.MakeTagValue(t)
767
-		}
768
-	}
769
-
770
-	// force trailing, if message requires it
758
+// SendRawMessage sends a raw message to the client.
759
+func (client *Client) SendRawMessage(message ircmsg.IrcMessage) error {
760
+	// use dumb hack to force the last param to be a trailing param if required
771
 	var usedTrailingHack bool
761
 	var usedTrailingHack bool
772
-	if commandsThatMustUseTrailing[strings.ToUpper(command)] && len(params) > 0 {
773
-		lastParam := params[len(params)-1]
762
+	if commandsThatMustUseTrailing[strings.ToUpper(message.Command)] && len(message.Params) > 0 {
763
+		lastParam := message.Params[len(message.Params)-1]
774
 		// to force trailing, we ensure the final param contains a space
764
 		// to force trailing, we ensure the final param contains a space
775
 		if !strings.Contains(lastParam, " ") {
765
 		if !strings.Contains(lastParam, " ") {
776
-			params[len(params)-1] = lastParam + " "
766
+			message.Params[len(message.Params)-1] = lastParam + " "
777
 			usedTrailingHack = true
767
 			usedTrailingHack = true
778
 		}
768
 		}
779
 	}
769
 	}
780
 
770
 
781
-	// send out the message
782
-	message := ircmsg.MakeMessage(tags, prefix, command, params...)
771
+	// assemble message
783
 	maxlenTags, maxlenRest := client.maxlens()
772
 	maxlenTags, maxlenRest := client.maxlens()
784
 	line, err := message.LineMaxLen(maxlenTags, maxlenRest)
773
 	line, err := message.LineMaxLen(maxlenTags, maxlenRest)
785
 	if err != nil {
774
 	if err != nil {
790
 
779
 
791
 		message = ircmsg.MakeMessage(nil, client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending")
780
 		message = ircmsg.MakeMessage(nil, client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending")
792
 		line, _ := message.Line()
781
 		line, _ := message.Line()
782
+
783
+		// if we used the trailing hack, we need to strip the final space we appended earlier on
784
+		if usedTrailingHack {
785
+			line = line[:len(line)-3] + "\r\n"
786
+		}
787
+
793
 		client.socket.Write(line)
788
 		client.socket.Write(line)
794
 		return err
789
 		return err
795
 	}
790
 	}
796
 
791
 
797
-	// is we used the trailing hack, we need to strip the final space we appended earlier
798
-	if usedTrailingHack {
799
-		line = line[:len(line)-3] + "\r\n"
800
-	}
801
-
802
 	client.server.logger.Debug("useroutput", client.nick, " ->", strings.TrimRight(line, "\r\n"))
792
 	client.server.logger.Debug("useroutput", client.nick, " ->", strings.TrimRight(line, "\r\n"))
803
 
793
 
804
 	client.socket.Write(line)
794
 	client.socket.Write(line)
795
+
796
+	return nil
797
+}
798
+
799
+// Send sends an IRC line to the client.
800
+func (client *Client) Send(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) error {
801
+	// attach server-time
802
+	if client.capabilities.Has(caps.ServerTime) {
803
+		t := time.Now().UTC().Format("2006-01-02T15:04:05.999Z")
804
+		if tags == nil {
805
+			tags = ircmsg.MakeTags("time", t)
806
+		} else {
807
+			(*tags)["time"] = ircmsg.MakeTagValue(t)
808
+		}
809
+	}
810
+
811
+	// send out the message
812
+	message := ircmsg.MakeMessage(tags, prefix, command, params...)
813
+	client.SendRawMessage(message)
805
 	return nil
814
 	return nil
806
 }
815
 }
807
 
816
 

+ 7
- 2
irc/commands.go View File

12
 
12
 
13
 // Command represents a command accepted from a client.
13
 // Command represents a command accepted from a client.
14
 type Command struct {
14
 type Command struct {
15
-	handler           func(server *Server, client *Client, msg ircmsg.IrcMessage) bool
15
+	handler           func(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool
16
 	oper              bool
16
 	oper              bool
17
 	usablePreReg      bool
17
 	usablePreReg      bool
18
 	leaveClientActive bool // if true, leaves the client active time alone. reversed because we can't default a struct element to True
18
 	leaveClientActive bool // if true, leaves the client active time alone. reversed because we can't default a struct element to True
45
 	if !cmd.leaveClientIdle {
45
 	if !cmd.leaveClientIdle {
46
 		client.Touch()
46
 		client.Touch()
47
 	}
47
 	}
48
-	exiting := cmd.handler(server, client, msg)
48
+	rb := NewResponseBuffer(client)
49
+	rb.Label = GetLabel(msg)
50
+
51
+	exiting := cmd.handler(server, client, msg, rb)
52
+
53
+	rb.Send()
49
 
54
 
50
 	// after each command, see if we can send registration to the client
55
 	// after each command, see if we can send registration to the client
51
 	if !client.registered {
56
 	if !client.registered {

+ 12
- 7
irc/handlers.go View File

2544
 		masksString = msg.Params[0]
2544
 		masksString = msg.Params[0]
2545
 	}
2545
 	}
2546
 
2546
 
2547
+	rb := NewResponseBuffer(client)
2548
+	rb.Label = GetLabel(msg)
2549
+
2547
 	if len(strings.TrimSpace(masksString)) < 1 {
2550
 	if len(strings.TrimSpace(masksString)) < 1 {
2548
-		client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, client.t("No masks given"))
2551
+		rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, client.t("No masks given"))
2552
+		rb.Send()
2549
 		return false
2553
 		return false
2550
 	}
2554
 	}
2551
 
2555
 
2554
 		for _, mask := range masks {
2558
 		for _, mask := range masks {
2555
 			casefoldedMask, err := Casefold(mask)
2559
 			casefoldedMask, err := Casefold(mask)
2556
 			if err != nil {
2560
 			if err != nil {
2557
-				client.Send(nil, client.server.name, ERR_NOSUCHNICK, client.nick, mask, client.t("No such nick"))
2561
+				rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.nick, mask, client.t("No such nick"))
2558
 				continue
2562
 				continue
2559
 			}
2563
 			}
2560
 			matches := server.clients.FindAll(casefoldedMask)
2564
 			matches := server.clients.FindAll(casefoldedMask)
2561
 			if len(matches) == 0 {
2565
 			if len(matches) == 0 {
2562
-				client.Send(nil, client.server.name, ERR_NOSUCHNICK, client.nick, mask, client.t("No such nick"))
2566
+				rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.nick, mask, client.t("No such nick"))
2563
 				continue
2567
 				continue
2564
 			}
2568
 			}
2565
 			for mclient := range matches {
2569
 			for mclient := range matches {
2566
-				client.getWhoisOf(mclient)
2570
+				client.getWhoisOf(mclient, rb)
2567
 			}
2571
 			}
2568
 		}
2572
 		}
2569
 	} else {
2573
 	} else {
2571
 		casefoldedMask, err := Casefold(strings.Split(masksString, ",")[0])
2575
 		casefoldedMask, err := Casefold(strings.Split(masksString, ",")[0])
2572
 		mclient := server.clients.Get(casefoldedMask)
2576
 		mclient := server.clients.Get(casefoldedMask)
2573
 		if err != nil || mclient == nil {
2577
 		if err != nil || mclient == nil {
2574
-			client.Send(nil, client.server.name, ERR_NOSUCHNICK, client.nick, masksString, client.t("No such nick"))
2578
+			rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.nick, masksString, client.t("No such nick"))
2575
 			// fall through, ENDOFWHOIS is always sent
2579
 			// fall through, ENDOFWHOIS is always sent
2576
 		} else {
2580
 		} else {
2577
-			client.getWhoisOf(mclient)
2581
+			client.getWhoisOf(mclient, rb)
2578
 		}
2582
 		}
2579
 	}
2583
 	}
2580
-	client.Send(nil, server.name, RPL_ENDOFWHOIS, client.nick, masksString, client.t("End of /WHOIS list"))
2584
+	rb.Add(nil, server.name, RPL_ENDOFWHOIS, client.nick, masksString, client.t("End of /WHOIS list"))
2585
+	rb.Send()
2581
 	return false
2586
 	return false
2582
 }
2587
 }
2583
 
2588
 

+ 1
- 1
irc/modes.go View File

214
 		if !hasPrivs(change) {
214
 		if !hasPrivs(change) {
215
 			if !alreadySentPrivError {
215
 			if !alreadySentPrivError {
216
 				alreadySentPrivError = true
216
 				alreadySentPrivError = true
217
-				client.Send(nil, client.server.name, ERR_CHANOPRIVSNEEDED, channel.name, client.t("You're not a channel operator"))
217
+				rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, channel.name, client.t("You're not a channel operator"))
218
 			}
218
 			}
219
 			continue
219
 			continue
220
 		}
220
 		}

+ 120
- 0
irc/responsebuffer.go View File

1
+// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"time"
8
+
9
+	"github.com/goshuirc/irc-go/ircmsg"
10
+	"github.com/oragono/oragono/irc/caps"
11
+)
12
+
13
+// ResponseBuffer - put simply - buffers messages and then outputs them to a given client.
14
+//
15
+// Using a ResponseBuffer lets you really easily implement labeled-response, since the
16
+// buffer will silently create a batch if required and label the outgoing messages as
17
+// necessary (or leave it off and simply tag the outgoing message).
18
+type ResponseBuffer struct {
19
+	Label    string
20
+	target   *Client
21
+	messages []ircmsg.IrcMessage
22
+}
23
+
24
+// GetLabel returns the label from the given message.
25
+func GetLabel(msg ircmsg.IrcMessage) string {
26
+	return msg.Tags["label"].Value
27
+}
28
+
29
+// NewResponseBuffer returns a new ResponseBuffer.
30
+func NewResponseBuffer(target *Client) *ResponseBuffer {
31
+	return &ResponseBuffer{
32
+		target: target,
33
+	}
34
+}
35
+
36
+// Add adds a standard new message to our queue.
37
+func (rb *ResponseBuffer) Add(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) {
38
+	message := ircmsg.MakeMessage(tags, prefix, command, params...)
39
+
40
+	rb.messages = append(rb.messages, message)
41
+}
42
+
43
+// AddFromClient adds a new message from a specific client to our queue.
44
+func (rb *ResponseBuffer) AddFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, params ...string) {
45
+	// attach account-tag
46
+	if rb.target.capabilities.Has(caps.AccountTag) && from.account != &NoAccount {
47
+		if tags == nil {
48
+			tags = ircmsg.MakeTags("account", from.account.Name)
49
+		} else {
50
+			(*tags)["account"] = ircmsg.MakeTagValue(from.account.Name)
51
+		}
52
+	}
53
+	// attach message-id
54
+	if len(msgid) > 0 && rb.target.capabilities.Has(caps.MessageTags) {
55
+		if tags == nil {
56
+			tags = ircmsg.MakeTags("draft/msgid", msgid)
57
+		} else {
58
+			(*tags)["draft/msgid"] = ircmsg.MakeTagValue(msgid)
59
+		}
60
+	}
61
+
62
+	rb.Add(tags, from.nickMaskString, command, params...)
63
+}
64
+
65
+// AddSplitMessageFromClient adds a new split message from a specific client to our queue.
66
+func (rb *ResponseBuffer) AddSplitMessageFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, target string, message SplitMessage) {
67
+	if rb.target.capabilities.Has(caps.MaxLine) {
68
+		rb.AddFromClient(msgid, from, tags, command, target, message.ForMaxLine)
69
+	} else {
70
+		for _, str := range message.For512 {
71
+			rb.AddFromClient(msgid, from, tags, command, target, str)
72
+		}
73
+	}
74
+}
75
+
76
+// Send sends the message to our target client.
77
+func (rb *ResponseBuffer) Send() error {
78
+	// make batch and all if required
79
+	var batch *Batch
80
+	useLabel := rb.target.capabilities.Has(caps.LabeledResponse) && rb.Label != ""
81
+	if useLabel && 1 < len(rb.messages) && rb.target.capabilities.Has(caps.Batch) {
82
+		batch = rb.target.server.batches.New("draft/labeled-response")
83
+	}
84
+
85
+	// if label but no batch, add label to first message
86
+	if useLabel && batch == nil {
87
+		message := rb.messages[0]
88
+		message.Tags["label"] = ircmsg.MakeTagValue(rb.Label)
89
+		rb.messages[0] = message
90
+	}
91
+
92
+	// start batch if required
93
+	if batch != nil {
94
+		batch.Start(rb.target, ircmsg.MakeTags("label", rb.Label))
95
+	}
96
+
97
+	// send each message out
98
+	for _, message := range rb.messages {
99
+		// attach server-time if needed
100
+		if rb.target.capabilities.Has(caps.ServerTime) {
101
+			t := time.Now().UTC().Format("2006-01-02T15:04:05.999Z")
102
+			message.Tags["time"] = ircmsg.MakeTagValue(t)
103
+		}
104
+
105
+		// attach batch ID
106
+		if batch != nil {
107
+			message.Tags["batch"] = ircmsg.MakeTagValue(batch.ID)
108
+		}
109
+
110
+		// send message out
111
+		rb.target.SendRawMessage(message)
112
+	}
113
+
114
+	// end batch if required
115
+	if batch != nil {
116
+		batch.End(rb.target)
117
+	}
118
+
119
+	return nil
120
+}

+ 14
- 12
irc/server.go View File

49
 
49
 
50
 	// SupportedCapabilities are the caps we advertise.
50
 	// SupportedCapabilities are the caps we advertise.
51
 	// MaxLine, SASL and STS are set during server startup.
51
 	// MaxLine, SASL and STS are set during server startup.
52
-	SupportedCapabilities = caps.NewSet(caps.AccountTag, caps.AccountNotify, caps.AwayNotify, caps.CapNotify, caps.ChgHost, caps.EchoMessage, caps.ExtendedJoin, caps.InviteNotify, caps.Languages, caps.MessageTags, caps.MultiPrefix, caps.Rename, caps.Resume, caps.ServerTime, caps.UserhostInNames)
52
+	SupportedCapabilities = caps.NewSet(caps.AccountTag, caps.AccountNotify, caps.AwayNotify, caps.Batch, caps.CapNotify, caps.ChgHost, caps.EchoMessage, caps.ExtendedJoin, caps.InviteNotify, caps.LabeledResponse, caps.Languages, caps.MessageTags, caps.MultiPrefix, caps.Rename, caps.Resume, caps.ServerTime, caps.UserhostInNames)
53
 
53
 
54
 	// CapValues are the actual values we advertise to v3.2 clients.
54
 	// CapValues are the actual values we advertise to v3.2 clients.
55
 	// actual values are set during server startup.
55
 	// actual values are set during server startup.
90
 	accountAuthenticationEnabled bool
90
 	accountAuthenticationEnabled bool
91
 	accountRegistration          *AccountRegistration
91
 	accountRegistration          *AccountRegistration
92
 	accounts                     map[string]*ClientAccount
92
 	accounts                     map[string]*ClientAccount
93
+	batches                      *BatchManager
93
 	channelRegistrationEnabled   bool
94
 	channelRegistrationEnabled   bool
94
 	channels                     *ChannelManager
95
 	channels                     *ChannelManager
95
 	channelRegistry              *ChannelRegistry
96
 	channelRegistry              *ChannelRegistry
150
 	// initialize data structures
151
 	// initialize data structures
151
 	server := &Server{
152
 	server := &Server{
152
 		accounts:            make(map[string]*ClientAccount),
153
 		accounts:            make(map[string]*ClientAccount),
154
+		batches:             NewBatchManager(),
153
 		channels:            NewChannelManager(),
155
 		channels:            NewChannelManager(),
154
 		clients:             NewClientManager(),
156
 		clients:             NewClientManager(),
155
 		connectionLimiter:   connection_limits.NewLimiter(),
157
 		connectionLimiter:   connection_limits.NewLimiter(),
603
 	return chstrs
605
 	return chstrs
604
 }
606
 }
605
 
607
 
606
-func (client *Client) getWhoisOf(target *Client) {
608
+func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
607
 	target.stateMutex.RLock()
609
 	target.stateMutex.RLock()
608
 	defer target.stateMutex.RUnlock()
610
 	defer target.stateMutex.RUnlock()
609
 
611
 
610
-	client.Send(nil, client.server.name, RPL_WHOISUSER, client.nick, target.nick, target.username, target.hostname, "*", target.realname)
612
+	rb.Add(nil, client.server.name, RPL_WHOISUSER, client.nick, target.nick, target.username, target.hostname, "*", target.realname)
611
 
613
 
612
 	whoischannels := client.WhoisChannelsNames(target)
614
 	whoischannels := client.WhoisChannelsNames(target)
613
 	if whoischannels != nil {
615
 	if whoischannels != nil {
614
-		client.Send(nil, client.server.name, RPL_WHOISCHANNELS, client.nick, target.nick, strings.Join(whoischannels, " "))
616
+		rb.Add(nil, client.server.name, RPL_WHOISCHANNELS, client.nick, target.nick, strings.Join(whoischannels, " "))
615
 	}
617
 	}
616
 	if target.class != nil {
618
 	if target.class != nil {
617
-		client.Send(nil, client.server.name, RPL_WHOISOPERATOR, client.nick, target.nick, target.whoisLine)
619
+		rb.Add(nil, client.server.name, RPL_WHOISOPERATOR, client.nick, target.nick, target.whoisLine)
618
 	}
620
 	}
619
 	if client.flags[modes.Operator] || client == target {
621
 	if client.flags[modes.Operator] || client == target {
620
-		client.Send(nil, client.server.name, RPL_WHOISACTUALLY, client.nick, target.nick, fmt.Sprintf("%s@%s", target.username, utils.LookupHostname(target.IPString())), target.IPString(), client.t("Actual user@host, Actual IP"))
622
+		rb.Add(nil, client.server.name, RPL_WHOISACTUALLY, client.nick, target.nick, fmt.Sprintf("%s@%s", target.username, utils.LookupHostname(target.IPString())), target.IPString(), client.t("Actual user@host, Actual IP"))
621
 	}
623
 	}
622
 	if target.flags[modes.TLS] {
624
 	if target.flags[modes.TLS] {
623
-		client.Send(nil, client.server.name, RPL_WHOISSECURE, client.nick, target.nick, client.t("is using a secure connection"))
625
+		rb.Add(nil, client.server.name, RPL_WHOISSECURE, client.nick, target.nick, client.t("is using a secure connection"))
624
 	}
626
 	}
625
 	accountName := target.AccountName()
627
 	accountName := target.AccountName()
626
 	if accountName != "" {
628
 	if accountName != "" {
627
-		client.Send(nil, client.server.name, RPL_WHOISACCOUNT, client.nick, accountName, client.t("is logged in as"))
629
+		rb.Add(nil, client.server.name, RPL_WHOISACCOUNT, client.nick, accountName, client.t("is logged in as"))
628
 	}
630
 	}
629
 	if target.flags[modes.Bot] {
631
 	if target.flags[modes.Bot] {
630
-		client.Send(nil, client.server.name, RPL_WHOISBOT, client.nick, target.nick, ircfmt.Unescape(fmt.Sprintf(client.t("is a $bBot$b on %s"), client.server.networkName)))
632
+		rb.Add(nil, client.server.name, RPL_WHOISBOT, client.nick, target.nick, ircfmt.Unescape(fmt.Sprintf(client.t("is a $bBot$b on %s"), client.server.networkName)))
631
 	}
633
 	}
632
 
634
 
633
 	if 0 < len(target.languages) {
635
 	if 0 < len(target.languages) {
636
 			params = append(params, str)
638
 			params = append(params, str)
637
 		}
639
 		}
638
 		params = append(params, client.t("can speak these languages"))
640
 		params = append(params, client.t("can speak these languages"))
639
-		client.Send(nil, client.server.name, RPL_WHOISLANGUAGE, params...)
641
+		rb.Add(nil, client.server.name, RPL_WHOISLANGUAGE, params...)
640
 	}
642
 	}
641
 
643
 
642
 	if target.certfp != "" && (client.flags[modes.Operator] || client == target) {
644
 	if target.certfp != "" && (client.flags[modes.Operator] || client == target) {
643
-		client.Send(nil, client.server.name, RPL_WHOISCERTFP, client.nick, target.nick, fmt.Sprintf(client.t("has client certificate fingerprint %s"), target.certfp))
645
+		rb.Add(nil, client.server.name, RPL_WHOISCERTFP, client.nick, target.nick, fmt.Sprintf(client.t("has client certificate fingerprint %s"), target.certfp))
644
 	}
646
 	}
645
-	client.Send(nil, client.server.name, RPL_WHOISIDLE, client.nick, target.nick, strconv.FormatUint(target.IdleSeconds(), 10), strconv.FormatInt(target.SignonTime(), 10), client.t("seconds idle, signon time"))
647
+	rb.Add(nil, client.server.name, RPL_WHOISIDLE, client.nick, target.nick, strconv.FormatUint(target.IdleSeconds(), 10), strconv.FormatInt(target.SignonTime(), 10), client.t("seconds idle, signon time"))
646
 }
648
 }
647
 
649
 
648
 // rplWhoReply returns the WHO reply between one user and another channel/user.
650
 // rplWhoReply returns the WHO reply between one user and another channel/user.

Loading…
Cancel
Save