source: code/trunk/irc.go@ 382

Last change on this file since 382 was 350, checked in by contact, 5 years ago

Sort and split JOIN messages

Sort channels so that channels with a key appear first. Split JOIN
messages so that we don't reach the message size limit.

File size: 8.8 KB
Line 
1package soju
2
3import (
4 "fmt"
5 "sort"
6 "strings"
7
8 "gopkg.in/irc.v3"
9)
10
11const (
12 rpl_statsping = "246"
13 rpl_localusers = "265"
14 rpl_globalusers = "266"
15 rpl_creationtime = "329"
16 rpl_topicwhotime = "333"
17 err_invalidcapcmd = "410"
18)
19
20const maxMessageLength = 512
21
22// The server-time layout, as defined in the IRCv3 spec.
23const serverTimeLayout = "2006-01-02T15:04:05.000Z"
24
25type userModes string
26
27func (ms userModes) Has(c byte) bool {
28 return strings.IndexByte(string(ms), c) >= 0
29}
30
31func (ms *userModes) Add(c byte) {
32 if !ms.Has(c) {
33 *ms += userModes(c)
34 }
35}
36
37func (ms *userModes) Del(c byte) {
38 i := strings.IndexByte(string(*ms), c)
39 if i >= 0 {
40 *ms = (*ms)[:i] + (*ms)[i+1:]
41 }
42}
43
44func (ms *userModes) Apply(s string) error {
45 var plusMinus byte
46 for i := 0; i < len(s); i++ {
47 switch c := s[i]; c {
48 case '+', '-':
49 plusMinus = c
50 default:
51 switch plusMinus {
52 case '+':
53 ms.Add(c)
54 case '-':
55 ms.Del(c)
56 default:
57 return fmt.Errorf("malformed modestring %q: missing plus/minus", s)
58 }
59 }
60 }
61 return nil
62}
63
64type channelModeType byte
65
66// standard channel mode types, as explained in https://modern.ircdocs.horse/#mode-message
67const (
68 // modes that add or remove an address to or from a list
69 modeTypeA channelModeType = iota
70 // modes that change a setting on a channel, and must always have a parameter
71 modeTypeB
72 // modes that change a setting on a channel, and must have a parameter when being set, and no parameter when being unset
73 modeTypeC
74 // modes that change a setting on a channel, and must not have a parameter
75 modeTypeD
76)
77
78var stdChannelModes = map[byte]channelModeType{
79 'b': modeTypeA, // ban list
80 'e': modeTypeA, // ban exception list
81 'I': modeTypeA, // invite exception list
82 'k': modeTypeB, // channel key
83 'l': modeTypeC, // channel user limit
84 'i': modeTypeD, // channel is invite-only
85 'm': modeTypeD, // channel is moderated
86 'n': modeTypeD, // channel has no external messages
87 's': modeTypeD, // channel is secret
88 't': modeTypeD, // channel has protected topic
89}
90
91type channelModes map[byte]string
92
93// applyChannelModes parses a mode string and mode arguments from a MODE message,
94// and applies the corresponding channel mode and user membership changes on that channel.
95//
96// If ch.modes is nil, channel modes are not updated.
97//
98// needMarshaling is a list of indexes of mode arguments that represent entities
99// that must be marshaled when sent downstream.
100func applyChannelModes(ch *upstreamChannel, modeStr string, arguments []string) (needMarshaling map[int]struct{}, err error) {
101 needMarshaling = make(map[int]struct{}, len(arguments))
102 nextArgument := 0
103 var plusMinus byte
104outer:
105 for i := 0; i < len(modeStr); i++ {
106 mode := modeStr[i]
107 if mode == '+' || mode == '-' {
108 plusMinus = mode
109 continue
110 }
111 if plusMinus != '+' && plusMinus != '-' {
112 return nil, fmt.Errorf("malformed modestring %q: missing plus/minus", modeStr)
113 }
114
115 for _, membership := range ch.conn.availableMemberships {
116 if membership.Mode == mode {
117 if nextArgument >= len(arguments) {
118 return nil, fmt.Errorf("malformed modestring %q: missing mode argument for %c%c", modeStr, plusMinus, mode)
119 }
120 member := arguments[nextArgument]
121 if _, ok := ch.Members[member]; ok {
122 if plusMinus == '+' {
123 ch.Members[member].Add(ch.conn.availableMemberships, membership)
124 } else {
125 // TODO: for upstreams without multi-prefix, query the user modes again
126 ch.Members[member].Remove(membership)
127 }
128 }
129 needMarshaling[nextArgument] = struct{}{}
130 nextArgument++
131 continue outer
132 }
133 }
134
135 mt, ok := ch.conn.availableChannelModes[mode]
136 if !ok {
137 continue
138 }
139 if mt == modeTypeB || (mt == modeTypeC && plusMinus == '+') {
140 if plusMinus == '+' {
141 var argument string
142 // some sentitive arguments (such as channel keys) can be omitted for privacy
143 // (this will only happen for RPL_CHANNELMODEIS, never for MODE messages)
144 if nextArgument < len(arguments) {
145 argument = arguments[nextArgument]
146 }
147 if ch.modes != nil {
148 ch.modes[mode] = argument
149 }
150 } else {
151 delete(ch.modes, mode)
152 }
153 nextArgument++
154 } else if mt == modeTypeC || mt == modeTypeD {
155 if plusMinus == '+' {
156 if ch.modes != nil {
157 ch.modes[mode] = ""
158 }
159 } else {
160 delete(ch.modes, mode)
161 }
162 }
163 }
164 return needMarshaling, nil
165}
166
167func (cm channelModes) Format() (modeString string, parameters []string) {
168 var modesWithValues strings.Builder
169 var modesWithoutValues strings.Builder
170 parameters = make([]string, 0, 16)
171 for mode, value := range cm {
172 if value != "" {
173 modesWithValues.WriteString(string(mode))
174 parameters = append(parameters, value)
175 } else {
176 modesWithoutValues.WriteString(string(mode))
177 }
178 }
179 modeString = "+" + modesWithValues.String() + modesWithoutValues.String()
180 return
181}
182
183const stdChannelTypes = "#&+!"
184
185type channelStatus byte
186
187const (
188 channelPublic channelStatus = '='
189 channelSecret channelStatus = '@'
190 channelPrivate channelStatus = '*'
191)
192
193func parseChannelStatus(s string) (channelStatus, error) {
194 if len(s) > 1 {
195 return 0, fmt.Errorf("invalid channel status %q: more than one character", s)
196 }
197 switch cs := channelStatus(s[0]); cs {
198 case channelPublic, channelSecret, channelPrivate:
199 return cs, nil
200 default:
201 return 0, fmt.Errorf("invalid channel status %q: unknown status", s)
202 }
203}
204
205type membership struct {
206 Mode byte
207 Prefix byte
208}
209
210var stdMemberships = []membership{
211 {'q', '~'}, // founder
212 {'a', '&'}, // protected
213 {'o', '@'}, // operator
214 {'h', '%'}, // halfop
215 {'v', '+'}, // voice
216}
217
218// memberships always sorted by descending membership rank
219type memberships []membership
220
221func (m *memberships) Add(availableMemberships []membership, newMembership membership) {
222 l := *m
223 i := 0
224 for _, availableMembership := range availableMemberships {
225 if i >= len(l) {
226 break
227 }
228 if l[i] == availableMembership {
229 if availableMembership == newMembership {
230 // we already have this membership
231 return
232 }
233 i++
234 continue
235 }
236 if availableMembership == newMembership {
237 break
238 }
239 }
240 // insert newMembership at i
241 l = append(l, membership{})
242 copy(l[i+1:], l[i:])
243 l[i] = newMembership
244 *m = l
245}
246
247func (m *memberships) Remove(oldMembership membership) {
248 l := *m
249 for i, currentMembership := range l {
250 if currentMembership == oldMembership {
251 *m = append(l[:i], l[i+1:]...)
252 return
253 }
254 }
255}
256
257func (m memberships) Format(dc *downstreamConn) string {
258 if !dc.caps["multi-prefix"] {
259 if len(m) == 0 {
260 return ""
261 }
262 return string(m[0].Prefix)
263 }
264 prefixes := make([]byte, len(m))
265 for i, membership := range m {
266 prefixes[i] = membership.Prefix
267 }
268 return string(prefixes)
269}
270
271func parseMessageParams(msg *irc.Message, out ...*string) error {
272 if len(msg.Params) < len(out) {
273 return newNeedMoreParamsError(msg.Command)
274 }
275 for i := range out {
276 if out[i] != nil {
277 *out[i] = msg.Params[i]
278 }
279 }
280 return nil
281}
282
283func copyClientTags(tags irc.Tags) irc.Tags {
284 t := make(irc.Tags, len(tags))
285 for k, v := range tags {
286 if strings.HasPrefix(k, "+") {
287 t[k] = v
288 }
289 }
290 return t
291}
292
293type batch struct {
294 Type string
295 Params []string
296 Outer *batch // if not-nil, this batch is nested in Outer
297 Label string
298}
299
300func join(channels, keys []string) []*irc.Message {
301 // Put channels with a key first
302 js := joinSorter{channels, keys}
303 sort.Sort(&js)
304
305 // Two spaces because there are three words (JOIN, channels and keys)
306 maxLength := maxMessageLength - (len("JOIN") + 2)
307
308 var msgs []*irc.Message
309 var channelsBuf, keysBuf strings.Builder
310 for i, channel := range channels {
311 key := keys[i]
312
313 n := channelsBuf.Len() + keysBuf.Len() + 1 + len(channel)
314 if key != "" {
315 n += 1 + len(key)
316 }
317
318 if channelsBuf.Len() > 0 && n > maxLength {
319 // No room for the new channel in this message
320 params := []string{channelsBuf.String()}
321 if keysBuf.Len() > 0 {
322 params = append(params, keysBuf.String())
323 }
324 msgs = append(msgs, &irc.Message{Command: "JOIN", Params: params})
325 channelsBuf.Reset()
326 keysBuf.Reset()
327 }
328
329 if channelsBuf.Len() > 0 {
330 channelsBuf.WriteByte(',')
331 }
332 channelsBuf.WriteString(channel)
333 if key != "" {
334 if keysBuf.Len() > 0 {
335 keysBuf.WriteByte(',')
336 }
337 keysBuf.WriteString(key)
338 }
339 }
340 if channelsBuf.Len() > 0 {
341 params := []string{channelsBuf.String()}
342 if keysBuf.Len() > 0 {
343 params = append(params, keysBuf.String())
344 }
345 msgs = append(msgs, &irc.Message{Command: "JOIN", Params: params})
346 }
347
348 return msgs
349}
350
351type joinSorter struct {
352 channels []string
353 keys []string
354}
355
356func (js *joinSorter) Len() int {
357 return len(js.channels)
358}
359
360func (js *joinSorter) Less(i, j int) bool {
361 if (js.keys[i] != "") != (js.keys[j] != "") {
362 // Only one of the channels has a key
363 return js.keys[i] != ""
364 }
365 return js.channels[i] < js.channels[j]
366}
367
368func (js *joinSorter) Swap(i, j int) {
369 js.channels[i], js.channels[j] = js.channels[j], js.channels[i]
370 js.keys[i], js.keys[j] = js.keys[j], js.keys[i]
371}
Note: See TracBrowser for help on using the repository browser.