|
@@ -0,0 +1,197 @@
|
|
1
|
+// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
|
2
|
+// released under the MIT license
|
|
3
|
+
|
|
4
|
+package irc
|
|
5
|
+
|
|
6
|
+import (
|
|
7
|
+ "time"
|
|
8
|
+
|
|
9
|
+ "github.com/goshuirc/irc-go/ircmsg"
|
|
10
|
+
|
|
11
|
+ "github.com/oragono/oragono/irc/caps"
|
|
12
|
+ "github.com/oragono/oragono/irc/utils"
|
|
13
|
+)
|
|
14
|
+
|
|
15
|
+// MessageCache caches serialized IRC messages.
|
|
16
|
+// First call Initialize or InitializeSplitMessage, which records
|
|
17
|
+// the parameters and builds the cache. Then call Send, which will
|
|
18
|
+// either send a cached version of the message or dispatch to another
|
|
19
|
+// send routine that can synthesize the necessary version on the fly.
|
|
20
|
+type MessageCache struct {
|
|
21
|
+ // these cache a single-line message (e.g., JOIN, or PRIVMSG with a 512-byte message)
|
|
22
|
+ // one version is "plain" (legacy clients with no tags) and one is "full" (client has
|
|
23
|
+ // the message-tags cap)
|
|
24
|
+ plain []byte
|
|
25
|
+ fullTags []byte
|
|
26
|
+ // these cache a multiline message (a PRIVMSG that was sent as a multiline batch)
|
|
27
|
+ // one version is "plain" (legacy clients with no tags) and one is "full" (client has
|
|
28
|
+ // the multiline cap)
|
|
29
|
+ plainMultiline [][]byte
|
|
30
|
+ fullTagsMultiline [][]byte
|
|
31
|
+
|
|
32
|
+ time time.Time
|
|
33
|
+ msgid string
|
|
34
|
+ accountName string
|
|
35
|
+ tags map[string]string
|
|
36
|
+ source string
|
|
37
|
+ command string
|
|
38
|
+
|
|
39
|
+ params []string
|
|
40
|
+
|
|
41
|
+ target string
|
|
42
|
+ splitMessage utils.SplitMessage
|
|
43
|
+}
|
|
44
|
+
|
|
45
|
+func addAllTags(msg *ircmsg.IrcMessage, tags map[string]string, serverTime time.Time, msgid, accountName string) {
|
|
46
|
+ msg.UpdateTags(tags)
|
|
47
|
+ msg.SetTag("time", serverTime.Format(IRCv3TimestampFormat))
|
|
48
|
+ if accountName != "*" {
|
|
49
|
+ msg.SetTag("account", accountName)
|
|
50
|
+ }
|
|
51
|
+ if msgid != "" {
|
|
52
|
+ msg.SetTag("msgid", msgid)
|
|
53
|
+ }
|
|
54
|
+}
|
|
55
|
+
|
|
56
|
+func (m *MessageCache) handleErr(server *Server, err error) bool {
|
|
57
|
+ if err != nil {
|
|
58
|
+ server.logger.Error("internal", "Error assembling message for sending", err.Error())
|
|
59
|
+ // blank these out so Send will be a no-op
|
|
60
|
+ m.fullTags = nil
|
|
61
|
+ m.fullTagsMultiline = nil
|
|
62
|
+ return true
|
|
63
|
+ }
|
|
64
|
+ return false
|
|
65
|
+}
|
|
66
|
+
|
|
67
|
+func (m *MessageCache) Initialize(server *Server, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) (err error) {
|
|
68
|
+ m.time = serverTime
|
|
69
|
+ m.msgid = msgid
|
|
70
|
+ m.source = nickmask
|
|
71
|
+ m.accountName = accountName
|
|
72
|
+ m.tags = tags
|
|
73
|
+ m.command = command
|
|
74
|
+ m.params = params
|
|
75
|
+
|
|
76
|
+ var msg ircmsg.IrcMessage
|
|
77
|
+ config := server.Config()
|
|
78
|
+ if config.Server.Compatibility.forceTrailing && commandsThatMustUseTrailing[command] {
|
|
79
|
+ msg.ForceTrailing()
|
|
80
|
+ }
|
|
81
|
+ msg.Prefix = nickmask
|
|
82
|
+ msg.Command = command
|
|
83
|
+ msg.Params = make([]string, len(params))
|
|
84
|
+ copy(msg.Params, params)
|
|
85
|
+ m.plain, err = msg.LineBytesStrict(false, MaxLineLen)
|
|
86
|
+ if m.handleErr(server, err) {
|
|
87
|
+ return
|
|
88
|
+ }
|
|
89
|
+
|
|
90
|
+ addAllTags(&msg, tags, serverTime, msgid, accountName)
|
|
91
|
+ m.fullTags, err = msg.LineBytesStrict(false, MaxLineLen)
|
|
92
|
+ if m.handleErr(server, err) {
|
|
93
|
+ return
|
|
94
|
+ }
|
|
95
|
+ return
|
|
96
|
+}
|
|
97
|
+
|
|
98
|
+func (m *MessageCache) InitializeSplitMessage(server *Server, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) (err error) {
|
|
99
|
+ m.time = message.Time
|
|
100
|
+ m.msgid = message.Msgid
|
|
101
|
+ m.source = nickmask
|
|
102
|
+ m.accountName = accountName
|
|
103
|
+ m.tags = tags
|
|
104
|
+ m.command = command
|
|
105
|
+ m.target = target
|
|
106
|
+ m.splitMessage = message
|
|
107
|
+
|
|
108
|
+ config := server.Config()
|
|
109
|
+ forceTrailing := config.Server.Compatibility.forceTrailing && commandsThatMustUseTrailing[command]
|
|
110
|
+
|
|
111
|
+ if message.Is512() {
|
|
112
|
+ isTagmsg := command == "TAGMSG"
|
|
113
|
+ var msg ircmsg.IrcMessage
|
|
114
|
+ if forceTrailing {
|
|
115
|
+ msg.ForceTrailing()
|
|
116
|
+ }
|
|
117
|
+
|
|
118
|
+ msg.Prefix = nickmask
|
|
119
|
+ msg.Command = command
|
|
120
|
+ if isTagmsg {
|
|
121
|
+ msg.Params = []string{target}
|
|
122
|
+ } else {
|
|
123
|
+ msg.Params = []string{target, message.Message}
|
|
124
|
+ }
|
|
125
|
+ m.params = msg.Params
|
|
126
|
+ if !isTagmsg {
|
|
127
|
+ m.plain, err = msg.LineBytesStrict(false, MaxLineLen)
|
|
128
|
+ if m.handleErr(server, err) {
|
|
129
|
+ return
|
|
130
|
+ }
|
|
131
|
+ }
|
|
132
|
+
|
|
133
|
+ addAllTags(&msg, tags, message.Time, message.Msgid, accountName)
|
|
134
|
+ m.fullTags, err = msg.LineBytesStrict(false, MaxLineLen)
|
|
135
|
+ if m.handleErr(server, err) {
|
|
136
|
+ return
|
|
137
|
+ }
|
|
138
|
+ } else {
|
|
139
|
+ var msg ircmsg.IrcMessage
|
|
140
|
+ if forceTrailing {
|
|
141
|
+ msg.ForceTrailing()
|
|
142
|
+ }
|
|
143
|
+ msg.Prefix = nickmask
|
|
144
|
+ msg.Command = command
|
|
145
|
+ msg.Params = make([]string, 2)
|
|
146
|
+ msg.Params[0] = target
|
|
147
|
+ m.plainMultiline = make([][]byte, len(message.Split))
|
|
148
|
+ for i, pair := range message.Split {
|
|
149
|
+ msg.Params[1] = pair.Message
|
|
150
|
+ m.plainMultiline[i], err = msg.LineBytesStrict(false, MaxLineLen)
|
|
151
|
+ if m.handleErr(server, err) {
|
|
152
|
+ return
|
|
153
|
+ }
|
|
154
|
+ }
|
|
155
|
+
|
|
156
|
+ // we need to send the same batch ID to all recipient sessions;
|
|
157
|
+ // use a uuidv4-alike to ensure that it won't collide
|
|
158
|
+ batch := composeMultilineBatch(utils.GenerateSecretToken(), nickmask, accountName, tags, command, target, message)
|
|
159
|
+ m.fullTagsMultiline = make([][]byte, len(batch))
|
|
160
|
+ for i, msg := range batch {
|
|
161
|
+ if forceTrailing {
|
|
162
|
+ msg.ForceTrailing()
|
|
163
|
+ }
|
|
164
|
+ m.fullTagsMultiline[i], err = msg.LineBytesStrict(false, MaxLineLen)
|
|
165
|
+ if m.handleErr(server, err) {
|
|
166
|
+ return
|
|
167
|
+ }
|
|
168
|
+ }
|
|
169
|
+ }
|
|
170
|
+ return
|
|
171
|
+}
|
|
172
|
+
|
|
173
|
+func (m *MessageCache) Send(session *Session) {
|
|
174
|
+ if m.fullTags != nil {
|
|
175
|
+ if session.capabilities.Has(caps.MessageTags) {
|
|
176
|
+ session.sendBytes(m.fullTags, false)
|
|
177
|
+ } else if !(session.capabilities.Has(caps.ServerTime) || session.capabilities.Has(caps.AccountTag)) {
|
|
178
|
+ if m.plain != nil {
|
|
179
|
+ session.sendBytes(m.plain, false)
|
|
180
|
+ }
|
|
181
|
+ } else {
|
|
182
|
+ session.sendFromClientInternal(false, m.time, m.msgid, m.source, m.accountName, nil, m.command, m.params...)
|
|
183
|
+ }
|
|
184
|
+ } else if m.fullTagsMultiline != nil {
|
|
185
|
+ if session.capabilities.Has(caps.Multiline) {
|
|
186
|
+ for _, line := range m.fullTagsMultiline {
|
|
187
|
+ session.sendBytes(line, false)
|
|
188
|
+ }
|
|
189
|
+ } else if !(session.capabilities.Has(caps.ServerTime) || session.capabilities.Has(caps.AccountTag)) {
|
|
190
|
+ for _, line := range m.plainMultiline {
|
|
191
|
+ session.sendBytes(line, false)
|
|
192
|
+ }
|
|
193
|
+ } else {
|
|
194
|
+ session.sendSplitMsgFromClientInternal(false, m.source, m.accountName, m.tags, m.command, m.target, m.splitMessage)
|
|
195
|
+ }
|
|
196
|
+ }
|
|
197
|
+}
|