source: code/trunk/irc.go@ 469

Last change on this file since 469 was 463, checked in by contact, 4 years ago

Passthrough some ISUPPORT tokens

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