Explorar el Código

Labeled-Response

tags/v0.11.0-beta
Daniel Oaks hace 6 años
padre
commit
07fc902b21
Se han modificado 5 ficheros con 257 adiciones y 43 borrados
  1. 78
    0
      irc/batch.go
  2. 1
    1
      irc/capability.go
  3. 33
    24
      irc/client.go
  4. 120
    0
      irc/responsebuffer.go
  5. 25
    18
      irc/server.go

+ 78
- 0
irc/batch.go Ver fichero

@@ -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(), 10) + strconv.FormatUint(bm.idCounter, 10)
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
+}

+ 1
- 1
irc/capability.go Ver fichero

@@ -14,7 +14,7 @@ import (
14 14
 var (
15 15
 	// SupportedCapabilities are the caps we advertise.
16 16
 	// MaxLine, SASL and STS are set during server startup.
17
-	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)
17
+	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)
18 18
 
19 19
 	// CapValues are the actual values we advertise to v3.2 clients.
20 20
 	// actual values are set during server startup.

+ 33
- 24
irc/client.go Ver fichero

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

+ 120
- 0
irc/responsebuffer.go Ver fichero

@@ -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
+}

+ 25
- 18
irc/server.go Ver fichero

@@ -78,6 +78,7 @@ type Server struct {
78 78
 	accountAuthenticationEnabled bool
79 79
 	accountRegistration          *AccountRegistration
80 80
 	accounts                     map[string]*ClientAccount
81
+	batches                      *BatchManager
81 82
 	channelRegistrationEnabled   bool
82 83
 	channels                     *ChannelManager
83 84
 	channelRegistry              *ChannelRegistry
@@ -138,6 +139,7 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
138 139
 	// initialize data structures
139 140
 	server := &Server{
140 141
 		accounts:            make(map[string]*ClientAccount),
142
+		batches:             NewBatchManager(),
141 143
 		channels:            NewChannelManager(),
142 144
 		clients:             NewClientManager(),
143 145
 		connectionLimiter:   connection_limits.NewLimiter(),
@@ -972,8 +974,12 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
972 974
 		masksString = msg.Params[0]
973 975
 	}
974 976
 
977
+	rb := NewResponseBuffer(client)
978
+	rb.Label = GetLabel(msg)
979
+
975 980
 	if len(strings.TrimSpace(masksString)) < 1 {
976
-		client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, client.t("No masks given"))
981
+		rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, client.t("No masks given"))
982
+		rb.Send()
977 983
 		return false
978 984
 	}
979 985
 
@@ -982,16 +988,16 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
982 988
 		for _, mask := range masks {
983 989
 			casefoldedMask, err := Casefold(mask)
984 990
 			if err != nil {
985
-				client.Send(nil, client.server.name, ERR_NOSUCHNICK, client.nick, mask, client.t("No such nick"))
991
+				rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.nick, mask, client.t("No such nick"))
986 992
 				continue
987 993
 			}
988 994
 			matches := server.clients.FindAll(casefoldedMask)
989 995
 			if len(matches) == 0 {
990
-				client.Send(nil, client.server.name, ERR_NOSUCHNICK, client.nick, mask, client.t("No such nick"))
996
+				rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.nick, mask, client.t("No such nick"))
991 997
 				continue
992 998
 			}
993 999
 			for mclient := range matches {
994
-				client.getWhoisOf(mclient)
1000
+				client.getWhoisOf(mclient, rb)
995 1001
 			}
996 1002
 		}
997 1003
 	} else {
@@ -999,41 +1005,42 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
999 1005
 		casefoldedMask, err := Casefold(strings.Split(masksString, ",")[0])
1000 1006
 		mclient := server.clients.Get(casefoldedMask)
1001 1007
 		if err != nil || mclient == nil {
1002
-			client.Send(nil, client.server.name, ERR_NOSUCHNICK, client.nick, masksString, client.t("No such nick"))
1008
+			rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.nick, masksString, client.t("No such nick"))
1003 1009
 			// fall through, ENDOFWHOIS is always sent
1004 1010
 		} else {
1005
-			client.getWhoisOf(mclient)
1011
+			client.getWhoisOf(mclient, rb)
1006 1012
 		}
1007 1013
 	}
1008
-	client.Send(nil, server.name, RPL_ENDOFWHOIS, client.nick, masksString, client.t("End of /WHOIS list"))
1014
+	rb.Add(nil, server.name, RPL_ENDOFWHOIS, client.nick, masksString, client.t("End of /WHOIS list"))
1015
+	rb.Send()
1009 1016
 	return false
1010 1017
 }
1011 1018
 
1012
-func (client *Client) getWhoisOf(target *Client) {
1019
+func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
1013 1020
 	target.stateMutex.RLock()
1014 1021
 	defer target.stateMutex.RUnlock()
1015 1022
 
1016
-	client.Send(nil, client.server.name, RPL_WHOISUSER, client.nick, target.nick, target.username, target.hostname, "*", target.realname)
1023
+	rb.Add(nil, client.server.name, RPL_WHOISUSER, client.nick, target.nick, target.username, target.hostname, "*", target.realname)
1017 1024
 
1018 1025
 	whoischannels := client.WhoisChannelsNames(target)
1019 1026
 	if whoischannels != nil {
1020
-		client.Send(nil, client.server.name, RPL_WHOISCHANNELS, client.nick, target.nick, strings.Join(whoischannels, " "))
1027
+		rb.Add(nil, client.server.name, RPL_WHOISCHANNELS, client.nick, target.nick, strings.Join(whoischannels, " "))
1021 1028
 	}
1022 1029
 	if target.class != nil {
1023
-		client.Send(nil, client.server.name, RPL_WHOISOPERATOR, client.nick, target.nick, target.whoisLine)
1030
+		rb.Add(nil, client.server.name, RPL_WHOISOPERATOR, client.nick, target.nick, target.whoisLine)
1024 1031
 	}
1025 1032
 	if client.flags[Operator] || client == target {
1026
-		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"))
1033
+		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"))
1027 1034
 	}
1028 1035
 	if target.flags[TLS] {
1029
-		client.Send(nil, client.server.name, RPL_WHOISSECURE, client.nick, target.nick, client.t("is using a secure connection"))
1036
+		rb.Add(nil, client.server.name, RPL_WHOISSECURE, client.nick, target.nick, client.t("is using a secure connection"))
1030 1037
 	}
1031 1038
 	accountName := target.AccountName()
1032 1039
 	if accountName != "" {
1033
-		client.Send(nil, client.server.name, RPL_WHOISACCOUNT, client.nick, accountName, client.t("is logged in as"))
1040
+		rb.Add(nil, client.server.name, RPL_WHOISACCOUNT, client.nick, accountName, client.t("is logged in as"))
1034 1041
 	}
1035 1042
 	if target.flags[Bot] {
1036
-		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)))
1043
+		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)))
1037 1044
 	}
1038 1045
 
1039 1046
 	if 0 < len(target.languages) {
@@ -1042,13 +1049,13 @@ func (client *Client) getWhoisOf(target *Client) {
1042 1049
 			params = append(params, str)
1043 1050
 		}
1044 1051
 		params = append(params, client.t("can speak these languages"))
1045
-		client.Send(nil, client.server.name, RPL_WHOISLANGUAGE, params...)
1052
+		rb.Add(nil, client.server.name, RPL_WHOISLANGUAGE, params...)
1046 1053
 	}
1047 1054
 
1048 1055
 	if target.certfp != "" && (client.flags[Operator] || client == target) {
1049
-		client.Send(nil, client.server.name, RPL_WHOISCERTFP, client.nick, target.nick, fmt.Sprintf(client.t("has client certificate fingerprint %s"), target.certfp))
1056
+		rb.Add(nil, client.server.name, RPL_WHOISCERTFP, client.nick, target.nick, fmt.Sprintf(client.t("has client certificate fingerprint %s"), target.certfp))
1050 1057
 	}
1051
-	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"))
1058
+	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"))
1052 1059
 }
1053 1060
 
1054 1061
 // rplWhoReply returns the WHO reply between one user and another channel/user.

Loading…
Cancelar
Guardar