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

@@ -0,0 +1,78 @@
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,7 +744,7 @@ func (client *Client) SendFromClient(msgid string, from *Client, tags *map[strin
744 744
 
745 745
 var (
746 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 748
 	// other params in messages.
749 749
 	commandsThatMustUseTrailing = map[string]bool{
750 750
 		"PRIVMSG": true,
@@ -755,31 +755,20 @@ var (
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 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 764
 		// to force trailing, we ensure the final param contains a space
775 765
 		if !strings.Contains(lastParam, " ") {
776
-			params[len(params)-1] = lastParam + " "
766
+			message.Params[len(message.Params)-1] = lastParam + " "
777 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 772
 	maxlenTags, maxlenRest := client.maxlens()
784 773
 	line, err := message.LineMaxLen(maxlenTags, maxlenRest)
785 774
 	if err != nil {
@@ -790,18 +779,38 @@ func (client *Client) Send(tags *map[string]ircmsg.TagValue, prefix string, comm
790 779
 
791 780
 		message = ircmsg.MakeMessage(nil, client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending")
792 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 788
 		client.socket.Write(line)
794 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 792
 	client.server.logger.Debug("useroutput", client.nick, " ->", strings.TrimRight(line, "\r\n"))
803 793
 
804 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 814
 	return nil
806 815
 }
807 816
 

+ 7
- 2
irc/commands.go View File

@@ -12,7 +12,7 @@ import (
12 12
 
13 13
 // Command represents a command accepted from a client.
14 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 16
 	oper              bool
17 17
 	usablePreReg      bool
18 18
 	leaveClientActive bool // if true, leaves the client active time alone. reversed because we can't default a struct element to True
@@ -45,7 +45,12 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b
45 45
 	if !cmd.leaveClientIdle {
46 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 55
 	// after each command, see if we can send registration to the client
51 56
 	if !client.registered {

+ 12
- 7
irc/handlers.go View File

@@ -2544,8 +2544,12 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
2544 2544
 		masksString = msg.Params[0]
2545 2545
 	}
2546 2546
 
2547
+	rb := NewResponseBuffer(client)
2548
+	rb.Label = GetLabel(msg)
2549
+
2547 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 2553
 		return false
2550 2554
 	}
2551 2555
 
@@ -2554,16 +2558,16 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
2554 2558
 		for _, mask := range masks {
2555 2559
 			casefoldedMask, err := Casefold(mask)
2556 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 2562
 				continue
2559 2563
 			}
2560 2564
 			matches := server.clients.FindAll(casefoldedMask)
2561 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 2567
 				continue
2564 2568
 			}
2565 2569
 			for mclient := range matches {
2566
-				client.getWhoisOf(mclient)
2570
+				client.getWhoisOf(mclient, rb)
2567 2571
 			}
2568 2572
 		}
2569 2573
 	} else {
@@ -2571,13 +2575,14 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
2571 2575
 		casefoldedMask, err := Casefold(strings.Split(masksString, ",")[0])
2572 2576
 		mclient := server.clients.Get(casefoldedMask)
2573 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 2579
 			// fall through, ENDOFWHOIS is always sent
2576 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 2586
 	return false
2582 2587
 }
2583 2588
 

+ 1
- 1
irc/modes.go View File

@@ -214,7 +214,7 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
214 214
 		if !hasPrivs(change) {
215 215
 			if !alreadySentPrivError {
216 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 219
 			continue
220 220
 		}

+ 120
- 0
irc/responsebuffer.go View File

@@ -0,0 +1,120 @@
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,7 +49,7 @@ var (
49 49
 
50 50
 	// SupportedCapabilities are the caps we advertise.
51 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 54
 	// CapValues are the actual values we advertise to v3.2 clients.
55 55
 	// actual values are set during server startup.
@@ -90,6 +90,7 @@ type Server struct {
90 90
 	accountAuthenticationEnabled bool
91 91
 	accountRegistration          *AccountRegistration
92 92
 	accounts                     map[string]*ClientAccount
93
+	batches                      *BatchManager
93 94
 	channelRegistrationEnabled   bool
94 95
 	channels                     *ChannelManager
95 96
 	channelRegistry              *ChannelRegistry
@@ -150,6 +151,7 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
150 151
 	// initialize data structures
151 152
 	server := &Server{
152 153
 		accounts:            make(map[string]*ClientAccount),
154
+		batches:             NewBatchManager(),
153 155
 		channels:            NewChannelManager(),
154 156
 		clients:             NewClientManager(),
155 157
 		connectionLimiter:   connection_limits.NewLimiter(),
@@ -603,31 +605,31 @@ func (client *Client) WhoisChannelsNames(target *Client) []string {
603 605
 	return chstrs
604 606
 }
605 607
 
606
-func (client *Client) getWhoisOf(target *Client) {
608
+func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
607 609
 	target.stateMutex.RLock()
608 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 614
 	whoischannels := client.WhoisChannelsNames(target)
613 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 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 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 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 627
 	accountName := target.AccountName()
626 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 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 635
 	if 0 < len(target.languages) {
@@ -636,13 +638,13 @@ func (client *Client) getWhoisOf(target *Client) {
636 638
 			params = append(params, str)
637 639
 		}
638 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 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 650
 // rplWhoReply returns the WHO reply between one user and another channel/user.

Loading…
Cancel
Save