source: code/trunk/irc.go@ 478

Last change on this file since 478 was 478, checked in by hubert, 4 years ago

Implement casemapping

TL;DR: supports for casemapping, now logs are saved in
casemapped/canonical/tolower form
(eg. in the #channel directory instead of #Channel... or something)

What is casemapping?

see <https://modern.ircdocs.horse/#casemapping-parameter>

Casemapping and multi-upstream

Since each upstream does not necessarily use the same casemapping, and
since casemappings cannot coexist [0],

  1. soju must also update the database accordingly to upstreams' casemapping, otherwise it will end up inconsistent,
  2. soju must "normalize" entity names and expose only one casemapping that is a subset of all supported casemappings (here, ascii).

[0] On some upstreams, "emersion[m]" and "emersion{m}" refer to the same
user (upstreams that advertise rfc1459 for example), while on others
(upstreams that advertise ascii) they don't.

Once upstream's casemapping is known (default to rfc1459), entity names
in map keys are made into casemapped form, for upstreamConn,
upstreamChannel and network.

downstreamConn advertises "CASEMAPPING=ascii", and always casemap map
keys with ascii.

Some functions require the caller to casemap their argument (to avoid
needless calls to casemapping functions).

Message forwarding and casemapping

downstream message handling (joins and parts basically):
When relaying entity names from downstreams to upstreams, soju uses the
upstream casemapping, in order to not get in the way of the user. This
does not brings any issue, as long as soju replies with the ascii
casemapping in mind (solves point 1.).

marshalEntity/marshalUserPrefix:
When relaying entity names from upstreams with non-ascii casemappings,
soju *partially* casemap them: it only change the case of characters
which are not ascii letters. ASCII case is thus kept intact, while
special symbols like []{} are the same every time soju sends them to
downstreams (solves point 2.).

Casemapping changes

Casemapping changes are not fully supported by this patch and will
result in loss of history. This is a limitation of the protocol and
should be solved by the RENAME spec.

File size: 13.9 KB
RevLine 
[98]1package soju
[20]2
3import (
4 "fmt"
[350]5 "sort"
[20]6 "strings"
[43]7
8 "gopkg.in/irc.v3"
[20]9)
10
11const (
[108]12 rpl_statsping = "246"
13 rpl_localusers = "265"
14 rpl_globalusers = "266"
[162]15 rpl_creationtime = "329"
[108]16 rpl_topicwhotime = "333"
17 err_invalidcapcmd = "410"
[20]18)
19
[463]20const (
21 maxMessageLength = 512
22 maxMessageParams = 15
23)
[346]24
[350]25// The server-time layout, as defined in the IRCv3 spec.
26const serverTimeLayout = "2006-01-02T15:04:05.000Z"
27
[139]28type userModes string
[20]29
[139]30func (ms userModes) Has(c byte) bool {
[20]31 return strings.IndexByte(string(ms), c) >= 0
32}
33
[139]34func (ms *userModes) Add(c byte) {
[20]35 if !ms.Has(c) {
[139]36 *ms += userModes(c)
[20]37 }
38}
39
[139]40func (ms *userModes) Del(c byte) {
[20]41 i := strings.IndexByte(string(*ms), c)
42 if i >= 0 {
43 *ms = (*ms)[:i] + (*ms)[i+1:]
44 }
45}
46
[139]47func (ms *userModes) Apply(s string) error {
[20]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
[139]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
[293]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))
[139]105 nextArgument := 0
106 var plusMinus byte
[293]107outer:
[139]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 != '-' {
[293]115 return nil, fmt.Errorf("malformed modestring %q: missing plus/minus", modeStr)
[139]116 }
117
[293]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]
[478]124 m := ch.Members.Value(member)
125 if m != nil {
[293]126 if plusMinus == '+' {
[478]127 m.Add(ch.conn.availableMemberships, membership)
[293]128 } else {
129 // TODO: for upstreams without multi-prefix, query the user modes again
[478]130 m.Remove(membership)
[293]131 }
132 }
133 needMarshaling[nextArgument] = struct{}{}
134 nextArgument++
135 continue outer
136 }
137 }
138
139 mt, ok := ch.conn.availableChannelModes[mode]
[139]140 if !ok {
141 continue
142 }
143 if mt == modeTypeB || (mt == modeTypeC && plusMinus == '+') {
144 if plusMinus == '+' {
145 var argument string
146 // some sentitive arguments (such as channel keys) can be omitted for privacy
147 // (this will only happen for RPL_CHANNELMODEIS, never for MODE messages)
148 if nextArgument < len(arguments) {
149 argument = arguments[nextArgument]
150 }
[293]151 if ch.modes != nil {
152 ch.modes[mode] = argument
153 }
[139]154 } else {
[293]155 delete(ch.modes, mode)
[139]156 }
157 nextArgument++
158 } else if mt == modeTypeC || mt == modeTypeD {
159 if plusMinus == '+' {
[293]160 if ch.modes != nil {
161 ch.modes[mode] = ""
162 }
[139]163 } else {
[293]164 delete(ch.modes, mode)
[139]165 }
166 }
167 }
[293]168 return needMarshaling, nil
[139]169}
170
171func (cm channelModes) Format() (modeString string, parameters []string) {
172 var modesWithValues strings.Builder
173 var modesWithoutValues strings.Builder
174 parameters = make([]string, 0, 16)
175 for mode, value := range cm {
176 if value != "" {
177 modesWithValues.WriteString(string(mode))
178 parameters = append(parameters, value)
179 } else {
180 modesWithoutValues.WriteString(string(mode))
181 }
182 }
183 modeString = "+" + modesWithValues.String() + modesWithoutValues.String()
184 return
185}
186
187const stdChannelTypes = "#&+!"
188
[20]189type channelStatus byte
190
191const (
192 channelPublic channelStatus = '='
193 channelSecret channelStatus = '@'
194 channelPrivate channelStatus = '*'
195)
196
197func parseChannelStatus(s string) (channelStatus, error) {
198 if len(s) > 1 {
199 return 0, fmt.Errorf("invalid channel status %q: more than one character", s)
200 }
201 switch cs := channelStatus(s[0]); cs {
202 case channelPublic, channelSecret, channelPrivate:
203 return cs, nil
204 default:
205 return 0, fmt.Errorf("invalid channel status %q: unknown status", s)
206 }
207}
208
[139]209type membership struct {
210 Mode byte
211 Prefix byte
212}
[20]213
[139]214var stdMemberships = []membership{
215 {'q', '~'}, // founder
216 {'a', '&'}, // protected
217 {'o', '@'}, // operator
218 {'h', '%'}, // halfop
219 {'v', '+'}, // voice
220}
[20]221
[292]222// memberships always sorted by descending membership rank
223type memberships []membership
224
225func (m *memberships) Add(availableMemberships []membership, newMembership membership) {
226 l := *m
227 i := 0
228 for _, availableMembership := range availableMemberships {
229 if i >= len(l) {
230 break
231 }
232 if l[i] == availableMembership {
233 if availableMembership == newMembership {
234 // we already have this membership
235 return
236 }
237 i++
238 continue
239 }
240 if availableMembership == newMembership {
241 break
242 }
[128]243 }
[292]244 // insert newMembership at i
245 l = append(l, membership{})
246 copy(l[i+1:], l[i:])
247 l[i] = newMembership
248 *m = l
[128]249}
250
[292]251func (m *memberships) Remove(oldMembership membership) {
252 l := *m
253 for i, currentMembership := range l {
254 if currentMembership == oldMembership {
255 *m = append(l[:i], l[i+1:]...)
256 return
257 }
258 }
259}
260
261func (m memberships) Format(dc *downstreamConn) string {
262 if !dc.caps["multi-prefix"] {
263 if len(m) == 0 {
264 return ""
265 }
266 return string(m[0].Prefix)
267 }
268 prefixes := make([]byte, len(m))
269 for i, membership := range m {
270 prefixes[i] = membership.Prefix
271 }
272 return string(prefixes)
273}
274
[43]275func parseMessageParams(msg *irc.Message, out ...*string) error {
276 if len(msg.Params) < len(out) {
277 return newNeedMoreParamsError(msg.Command)
278 }
279 for i := range out {
280 if out[i] != nil {
281 *out[i] = msg.Params[i]
282 }
283 }
284 return nil
285}
[153]286
[303]287func copyClientTags(tags irc.Tags) irc.Tags {
288 t := make(irc.Tags, len(tags))
289 for k, v := range tags {
290 if strings.HasPrefix(k, "+") {
291 t[k] = v
292 }
293 }
294 return t
295}
296
[153]297type batch struct {
298 Type string
299 Params []string
300 Outer *batch // if not-nil, this batch is nested in Outer
[155]301 Label string
[153]302}
[193]303
[350]304func join(channels, keys []string) []*irc.Message {
305 // Put channels with a key first
306 js := joinSorter{channels, keys}
307 sort.Sort(&js)
308
309 // Two spaces because there are three words (JOIN, channels and keys)
310 maxLength := maxMessageLength - (len("JOIN") + 2)
311
312 var msgs []*irc.Message
313 var channelsBuf, keysBuf strings.Builder
314 for i, channel := range channels {
315 key := keys[i]
316
317 n := channelsBuf.Len() + keysBuf.Len() + 1 + len(channel)
318 if key != "" {
319 n += 1 + len(key)
320 }
321
322 if channelsBuf.Len() > 0 && n > maxLength {
323 // No room for the new channel in this message
324 params := []string{channelsBuf.String()}
325 if keysBuf.Len() > 0 {
326 params = append(params, keysBuf.String())
327 }
328 msgs = append(msgs, &irc.Message{Command: "JOIN", Params: params})
329 channelsBuf.Reset()
330 keysBuf.Reset()
331 }
332
333 if channelsBuf.Len() > 0 {
334 channelsBuf.WriteByte(',')
335 }
336 channelsBuf.WriteString(channel)
337 if key != "" {
338 if keysBuf.Len() > 0 {
339 keysBuf.WriteByte(',')
340 }
341 keysBuf.WriteString(key)
342 }
343 }
344 if channelsBuf.Len() > 0 {
345 params := []string{channelsBuf.String()}
346 if keysBuf.Len() > 0 {
347 params = append(params, keysBuf.String())
348 }
349 msgs = append(msgs, &irc.Message{Command: "JOIN", Params: params})
350 }
351
352 return msgs
353}
354
[463]355func generateIsupport(prefix *irc.Prefix, nick string, tokens []string) []*irc.Message {
356 maxTokens := maxMessageParams - 2 // 2 reserved params: nick + text
357
358 var msgs []*irc.Message
359 for len(tokens) > 0 {
360 var msgTokens []string
361 if len(tokens) > maxTokens {
362 msgTokens = tokens[:maxTokens]
363 tokens = tokens[maxTokens:]
364 } else {
365 msgTokens = tokens
366 tokens = nil
367 }
368
369 msgs = append(msgs, &irc.Message{
370 Prefix: prefix,
371 Command: irc.RPL_ISUPPORT,
372 Params: append(append([]string{nick}, msgTokens...), "are supported"),
373 })
374 }
375
376 return msgs
377}
378
[350]379type joinSorter struct {
380 channels []string
381 keys []string
382}
383
384func (js *joinSorter) Len() int {
385 return len(js.channels)
386}
387
388func (js *joinSorter) Less(i, j int) bool {
389 if (js.keys[i] != "") != (js.keys[j] != "") {
390 // Only one of the channels has a key
391 return js.keys[i] != ""
392 }
393 return js.channels[i] < js.channels[j]
394}
395
396func (js *joinSorter) Swap(i, j int) {
397 js.channels[i], js.channels[j] = js.channels[j], js.channels[i]
398 js.keys[i], js.keys[j] = js.keys[j], js.keys[i]
399}
[392]400
401// parseCTCPMessage parses a CTCP message. CTCP is defined in
402// https://tools.ietf.org/html/draft-oakley-irc-ctcp-02
403func parseCTCPMessage(msg *irc.Message) (cmd string, params string, ok bool) {
404 if (msg.Command != "PRIVMSG" && msg.Command != "NOTICE") || len(msg.Params) < 2 {
405 return "", "", false
406 }
407 text := msg.Params[1]
408
409 if !strings.HasPrefix(text, "\x01") {
410 return "", "", false
411 }
412 text = strings.Trim(text, "\x01")
413
414 words := strings.SplitN(text, " ", 2)
415 cmd = strings.ToUpper(words[0])
416 if len(words) > 1 {
417 params = words[1]
418 }
419
420 return cmd, params, true
421}
[478]422
423type casemapping func(string) string
424
425func casemapNone(name string) string {
426 return name
427}
428
429// CasemapASCII of name is the canonical representation of name according to the
430// ascii casemapping.
431func casemapASCII(name string) string {
432 var sb strings.Builder
433 sb.Grow(len(name))
434 for _, r := range name {
435 if 'A' <= r && r <= 'Z' {
436 r += 'a' - 'A'
437 }
438 sb.WriteRune(r)
439 }
440 return sb.String()
441}
442
443// casemapRFC1459 of name is the canonical representation of name according to the
444// rfc1459 casemapping.
445func casemapRFC1459(name string) string {
446 var sb strings.Builder
447 sb.Grow(len(name))
448 for _, r := range name {
449 if 'A' <= r && r <= 'Z' {
450 r += 'a' - 'A'
451 } else if r == '{' {
452 r = '['
453 } else if r == '}' {
454 r = ']'
455 } else if r == '\\' {
456 r = '|'
457 } else if r == '~' {
458 r = '^'
459 }
460 sb.WriteRune(r)
461 }
462 return sb.String()
463}
464
465// casemapRFC1459Strict of name is the canonical representation of name
466// according to the rfc1459-strict casemapping.
467func casemapRFC1459Strict(name string) string {
468 var sb strings.Builder
469 sb.Grow(len(name))
470 for _, r := range name {
471 if 'A' <= r && r <= 'Z' {
472 r += 'a' - 'A'
473 } else if r == '{' {
474 r = '['
475 } else if r == '}' {
476 r = ']'
477 } else if r == '\\' {
478 r = '|'
479 }
480 sb.WriteRune(r)
481 }
482 return sb.String()
483}
484
485func parseCasemappingToken(tokenValue string) (casemap casemapping, ok bool) {
486 switch tokenValue {
487 case "ascii":
488 casemap = casemapASCII
489 case "rfc1459":
490 casemap = casemapRFC1459
491 case "rfc1459-strict":
492 casemap = casemapRFC1459Strict
493 default:
494 return nil, false
495 }
496 return casemap, true
497}
498
499func partialCasemap(higher casemapping, name string) string {
500 nameFullyCM := higher(name)
501 var sb strings.Builder
502 sb.Grow(len(name))
503 for i, r := range nameFullyCM {
504 if 'a' <= r && r <= 'z' {
505 r = rune(name[i])
506 }
507 sb.WriteRune(r)
508 }
509 return sb.String()
510}
511
512type casemapMap struct {
513 innerMap map[string]casemapEntry
514 casemap casemapping
515}
516
517type casemapEntry struct {
518 originalKey string
519 value interface{}
520}
521
522func newCasemapMap(size int) casemapMap {
523 return casemapMap{
524 innerMap: make(map[string]casemapEntry, size),
525 casemap: casemapNone,
526 }
527}
528
529func (cm *casemapMap) OriginalKey(name string) (key string, ok bool) {
530 entry, ok := cm.innerMap[cm.casemap(name)]
531 if !ok {
532 return "", false
533 }
534 return entry.originalKey, true
535}
536
537func (cm *casemapMap) Has(name string) bool {
538 _, ok := cm.innerMap[cm.casemap(name)]
539 return ok
540}
541
542func (cm *casemapMap) Len() int {
543 return len(cm.innerMap)
544}
545
546func (cm *casemapMap) SetValue(name string, value interface{}) {
547 nameCM := cm.casemap(name)
548 entry, ok := cm.innerMap[nameCM]
549 if !ok {
550 cm.innerMap[nameCM] = casemapEntry{
551 originalKey: name,
552 value: value,
553 }
554 return
555 }
556 entry.value = value
557 cm.innerMap[nameCM] = entry
558}
559
560func (cm *casemapMap) Delete(name string) {
561 delete(cm.innerMap, cm.casemap(name))
562}
563
564func (cm *casemapMap) SetCasemapping(newCasemap casemapping) {
565 cm.casemap = newCasemap
566 newInnerMap := make(map[string]casemapEntry, len(cm.innerMap))
567 for _, entry := range cm.innerMap {
568 newInnerMap[cm.casemap(entry.originalKey)] = entry
569 }
570 cm.innerMap = newInnerMap
571}
572
573type upstreamChannelCasemapMap struct{ casemapMap }
574
575func (cm *upstreamChannelCasemapMap) Value(name string) *upstreamChannel {
576 entry, ok := cm.innerMap[cm.casemap(name)]
577 if !ok {
578 return nil
579 }
580 return entry.value.(*upstreamChannel)
581}
582
583type channelCasemapMap struct{ casemapMap }
584
585func (cm *channelCasemapMap) Value(name string) *Channel {
586 entry, ok := cm.innerMap[cm.casemap(name)]
587 if !ok {
588 return nil
589 }
590 return entry.value.(*Channel)
591}
592
593type membershipsCasemapMap struct{ casemapMap }
594
595func (cm *membershipsCasemapMap) Value(name string) *memberships {
596 entry, ok := cm.innerMap[cm.casemap(name)]
597 if !ok {
598 return nil
599 }
600 return entry.value.(*memberships)
601}
602
603type mapStringStringCasemapMap struct{ casemapMap }
604
605func (cm *mapStringStringCasemapMap) Value(name string) map[string]string {
606 entry, ok := cm.innerMap[cm.casemap(name)]
607 if !ok {
608 return nil
609 }
610 return entry.value.(map[string]string)
611}
Note: See TracBrowser for help on using the repository browser.