source: code/trunk/irc.go@ 683

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

Skip list/type A mode arguments

Type-A modes always have an argument[0], but soju doesn't care about
them since it doesn't keep track of mode lists (ban/invite/.. lists).

[0] https://modern.ircdocs.horse/#mode-message

Type A: Modes that add or remove an address to or from a list. These
modes MUST always have a parameter when sent from the server to a
client.

File size: 17.4 KB
RevLine 
[98]1package soju
[20]2
3import (
4 "fmt"
[350]5 "sort"
[20]6 "strings"
[516]7 "time"
[498]8 "unicode"
9 "unicode/utf8"
[43]10
11 "gopkg.in/irc.v3"
[20]12)
13
14const (
[108]15 rpl_statsping = "246"
16 rpl_localusers = "265"
17 rpl_globalusers = "266"
[162]18 rpl_creationtime = "329"
[108]19 rpl_topicwhotime = "333"
[660]20 rpl_whospcrpl = "354"
[661]21 rpl_whoisaccount = "330"
[108]22 err_invalidcapcmd = "410"
[20]23)
24
[463]25const (
26 maxMessageLength = 512
27 maxMessageParams = 15
28)
[346]29
[350]30// The server-time layout, as defined in the IRCv3 spec.
31const serverTimeLayout = "2006-01-02T15:04:05.000Z"
32
[139]33type userModes string
[20]34
[139]35func (ms userModes) Has(c byte) bool {
[20]36 return strings.IndexByte(string(ms), c) >= 0
37}
38
[139]39func (ms *userModes) Add(c byte) {
[20]40 if !ms.Has(c) {
[139]41 *ms += userModes(c)
[20]42 }
43}
44
[139]45func (ms *userModes) Del(c byte) {
[20]46 i := strings.IndexByte(string(*ms), c)
47 if i >= 0 {
48 *ms = (*ms)[:i] + (*ms)[i+1:]
49 }
50}
51
[139]52func (ms *userModes) Apply(s string) error {
[20]53 var plusMinus byte
54 for i := 0; i < len(s); i++ {
55 switch c := s[i]; c {
56 case '+', '-':
57 plusMinus = c
58 default:
59 switch plusMinus {
60 case '+':
61 ms.Add(c)
62 case '-':
63 ms.Del(c)
64 default:
65 return fmt.Errorf("malformed modestring %q: missing plus/minus", s)
66 }
67 }
68 }
69 return nil
70}
71
[139]72type channelModeType byte
73
74// standard channel mode types, as explained in https://modern.ircdocs.horse/#mode-message
75const (
76 // modes that add or remove an address to or from a list
77 modeTypeA channelModeType = iota
78 // modes that change a setting on a channel, and must always have a parameter
79 modeTypeB
80 // modes that change a setting on a channel, and must have a parameter when being set, and no parameter when being unset
81 modeTypeC
82 // modes that change a setting on a channel, and must not have a parameter
83 modeTypeD
84)
85
86var stdChannelModes = map[byte]channelModeType{
87 'b': modeTypeA, // ban list
88 'e': modeTypeA, // ban exception list
89 'I': modeTypeA, // invite exception list
90 'k': modeTypeB, // channel key
91 'l': modeTypeC, // channel user limit
92 'i': modeTypeD, // channel is invite-only
93 'm': modeTypeD, // channel is moderated
94 'n': modeTypeD, // channel has no external messages
95 's': modeTypeD, // channel is secret
96 't': modeTypeD, // channel has protected topic
97}
98
99type channelModes map[byte]string
100
[293]101// applyChannelModes parses a mode string and mode arguments from a MODE message,
102// and applies the corresponding channel mode and user membership changes on that channel.
103//
104// If ch.modes is nil, channel modes are not updated.
105//
106// needMarshaling is a list of indexes of mode arguments that represent entities
107// that must be marshaled when sent downstream.
108func applyChannelModes(ch *upstreamChannel, modeStr string, arguments []string) (needMarshaling map[int]struct{}, err error) {
109 needMarshaling = make(map[int]struct{}, len(arguments))
[139]110 nextArgument := 0
111 var plusMinus byte
[293]112outer:
[139]113 for i := 0; i < len(modeStr); i++ {
114 mode := modeStr[i]
115 if mode == '+' || mode == '-' {
116 plusMinus = mode
117 continue
118 }
119 if plusMinus != '+' && plusMinus != '-' {
[293]120 return nil, fmt.Errorf("malformed modestring %q: missing plus/minus", modeStr)
[139]121 }
122
[293]123 for _, membership := range ch.conn.availableMemberships {
124 if membership.Mode == mode {
125 if nextArgument >= len(arguments) {
126 return nil, fmt.Errorf("malformed modestring %q: missing mode argument for %c%c", modeStr, plusMinus, mode)
127 }
128 member := arguments[nextArgument]
[478]129 m := ch.Members.Value(member)
130 if m != nil {
[293]131 if plusMinus == '+' {
[478]132 m.Add(ch.conn.availableMemberships, membership)
[293]133 } else {
134 // TODO: for upstreams without multi-prefix, query the user modes again
[478]135 m.Remove(membership)
[293]136 }
137 }
138 needMarshaling[nextArgument] = struct{}{}
139 nextArgument++
140 continue outer
141 }
142 }
143
144 mt, ok := ch.conn.availableChannelModes[mode]
[139]145 if !ok {
146 continue
147 }
[673]148 if mt == modeTypeA {
149 nextArgument++
150 } else if mt == modeTypeB || (mt == modeTypeC && plusMinus == '+') {
[139]151 if plusMinus == '+' {
152 var argument string
153 // some sentitive arguments (such as channel keys) can be omitted for privacy
154 // (this will only happen for RPL_CHANNELMODEIS, never for MODE messages)
155 if nextArgument < len(arguments) {
156 argument = arguments[nextArgument]
157 }
[293]158 if ch.modes != nil {
159 ch.modes[mode] = argument
160 }
[139]161 } else {
[293]162 delete(ch.modes, mode)
[139]163 }
164 nextArgument++
165 } else if mt == modeTypeC || mt == modeTypeD {
166 if plusMinus == '+' {
[293]167 if ch.modes != nil {
168 ch.modes[mode] = ""
169 }
[139]170 } else {
[293]171 delete(ch.modes, mode)
[139]172 }
173 }
174 }
[293]175 return needMarshaling, nil
[139]176}
177
178func (cm channelModes) Format() (modeString string, parameters []string) {
179 var modesWithValues strings.Builder
180 var modesWithoutValues strings.Builder
181 parameters = make([]string, 0, 16)
182 for mode, value := range cm {
183 if value != "" {
184 modesWithValues.WriteString(string(mode))
185 parameters = append(parameters, value)
186 } else {
187 modesWithoutValues.WriteString(string(mode))
188 }
189 }
190 modeString = "+" + modesWithValues.String() + modesWithoutValues.String()
191 return
192}
193
194const stdChannelTypes = "#&+!"
195
[20]196type channelStatus byte
197
198const (
199 channelPublic channelStatus = '='
200 channelSecret channelStatus = '@'
201 channelPrivate channelStatus = '*'
202)
203
204func parseChannelStatus(s string) (channelStatus, error) {
205 if len(s) > 1 {
206 return 0, fmt.Errorf("invalid channel status %q: more than one character", s)
207 }
208 switch cs := channelStatus(s[0]); cs {
209 case channelPublic, channelSecret, channelPrivate:
210 return cs, nil
211 default:
212 return 0, fmt.Errorf("invalid channel status %q: unknown status", s)
213 }
214}
215
[139]216type membership struct {
217 Mode byte
218 Prefix byte
219}
[20]220
[139]221var stdMemberships = []membership{
222 {'q', '~'}, // founder
223 {'a', '&'}, // protected
224 {'o', '@'}, // operator
225 {'h', '%'}, // halfop
226 {'v', '+'}, // voice
227}
[20]228
[292]229// memberships always sorted by descending membership rank
230type memberships []membership
231
232func (m *memberships) Add(availableMemberships []membership, newMembership membership) {
233 l := *m
234 i := 0
235 for _, availableMembership := range availableMemberships {
236 if i >= len(l) {
237 break
238 }
239 if l[i] == availableMembership {
240 if availableMembership == newMembership {
241 // we already have this membership
242 return
243 }
244 i++
245 continue
246 }
247 if availableMembership == newMembership {
248 break
249 }
[128]250 }
[292]251 // insert newMembership at i
252 l = append(l, membership{})
253 copy(l[i+1:], l[i:])
254 l[i] = newMembership
255 *m = l
[128]256}
257
[292]258func (m *memberships) Remove(oldMembership membership) {
259 l := *m
260 for i, currentMembership := range l {
261 if currentMembership == oldMembership {
262 *m = append(l[:i], l[i+1:]...)
263 return
264 }
265 }
266}
267
268func (m memberships) Format(dc *downstreamConn) string {
269 if !dc.caps["multi-prefix"] {
270 if len(m) == 0 {
271 return ""
272 }
273 return string(m[0].Prefix)
274 }
275 prefixes := make([]byte, len(m))
276 for i, membership := range m {
277 prefixes[i] = membership.Prefix
278 }
279 return string(prefixes)
280}
281
[43]282func parseMessageParams(msg *irc.Message, out ...*string) error {
283 if len(msg.Params) < len(out) {
284 return newNeedMoreParamsError(msg.Command)
285 }
286 for i := range out {
287 if out[i] != nil {
288 *out[i] = msg.Params[i]
289 }
290 }
291 return nil
292}
[153]293
[303]294func copyClientTags(tags irc.Tags) irc.Tags {
295 t := make(irc.Tags, len(tags))
296 for k, v := range tags {
297 if strings.HasPrefix(k, "+") {
298 t[k] = v
299 }
300 }
301 return t
302}
303
[153]304type batch struct {
305 Type string
306 Params []string
307 Outer *batch // if not-nil, this batch is nested in Outer
[155]308 Label string
[153]309}
[193]310
[350]311func join(channels, keys []string) []*irc.Message {
312 // Put channels with a key first
313 js := joinSorter{channels, keys}
314 sort.Sort(&js)
315
316 // Two spaces because there are three words (JOIN, channels and keys)
317 maxLength := maxMessageLength - (len("JOIN") + 2)
318
319 var msgs []*irc.Message
320 var channelsBuf, keysBuf strings.Builder
321 for i, channel := range channels {
322 key := keys[i]
323
324 n := channelsBuf.Len() + keysBuf.Len() + 1 + len(channel)
325 if key != "" {
326 n += 1 + len(key)
327 }
328
329 if channelsBuf.Len() > 0 && n > maxLength {
330 // No room for the new channel in this message
331 params := []string{channelsBuf.String()}
332 if keysBuf.Len() > 0 {
333 params = append(params, keysBuf.String())
334 }
335 msgs = append(msgs, &irc.Message{Command: "JOIN", Params: params})
336 channelsBuf.Reset()
337 keysBuf.Reset()
338 }
339
340 if channelsBuf.Len() > 0 {
341 channelsBuf.WriteByte(',')
342 }
343 channelsBuf.WriteString(channel)
344 if key != "" {
345 if keysBuf.Len() > 0 {
346 keysBuf.WriteByte(',')
347 }
348 keysBuf.WriteString(key)
349 }
350 }
351 if channelsBuf.Len() > 0 {
352 params := []string{channelsBuf.String()}
353 if keysBuf.Len() > 0 {
354 params = append(params, keysBuf.String())
355 }
356 msgs = append(msgs, &irc.Message{Command: "JOIN", Params: params})
357 }
358
359 return msgs
360}
361
[463]362func generateIsupport(prefix *irc.Prefix, nick string, tokens []string) []*irc.Message {
363 maxTokens := maxMessageParams - 2 // 2 reserved params: nick + text
364
365 var msgs []*irc.Message
366 for len(tokens) > 0 {
367 var msgTokens []string
368 if len(tokens) > maxTokens {
369 msgTokens = tokens[:maxTokens]
370 tokens = tokens[maxTokens:]
371 } else {
372 msgTokens = tokens
373 tokens = nil
374 }
375
376 msgs = append(msgs, &irc.Message{
377 Prefix: prefix,
378 Command: irc.RPL_ISUPPORT,
379 Params: append(append([]string{nick}, msgTokens...), "are supported"),
380 })
381 }
382
383 return msgs
384}
385
[636]386func generateMOTD(prefix *irc.Prefix, nick string, motd string) []*irc.Message {
387 var msgs []*irc.Message
388 msgs = append(msgs, &irc.Message{
389 Prefix: prefix,
390 Command: irc.RPL_MOTDSTART,
391 Params: []string{nick, fmt.Sprintf("- Message of the Day -")},
392 })
393
394 for _, l := range strings.Split(motd, "\n") {
395 msgs = append(msgs, &irc.Message{
396 Prefix: prefix,
397 Command: irc.RPL_MOTD,
398 Params: []string{nick, l},
399 })
400 }
401
402 msgs = append(msgs, &irc.Message{
403 Prefix: prefix,
404 Command: irc.RPL_ENDOFMOTD,
405 Params: []string{nick, "End of /MOTD command."},
406 })
407
408 return msgs
409}
410
[350]411type joinSorter struct {
412 channels []string
413 keys []string
414}
415
416func (js *joinSorter) Len() int {
417 return len(js.channels)
418}
419
420func (js *joinSorter) Less(i, j int) bool {
421 if (js.keys[i] != "") != (js.keys[j] != "") {
422 // Only one of the channels has a key
423 return js.keys[i] != ""
424 }
425 return js.channels[i] < js.channels[j]
426}
427
428func (js *joinSorter) Swap(i, j int) {
429 js.channels[i], js.channels[j] = js.channels[j], js.channels[i]
430 js.keys[i], js.keys[j] = js.keys[j], js.keys[i]
431}
[392]432
433// parseCTCPMessage parses a CTCP message. CTCP is defined in
434// https://tools.ietf.org/html/draft-oakley-irc-ctcp-02
435func parseCTCPMessage(msg *irc.Message) (cmd string, params string, ok bool) {
436 if (msg.Command != "PRIVMSG" && msg.Command != "NOTICE") || len(msg.Params) < 2 {
437 return "", "", false
438 }
439 text := msg.Params[1]
440
441 if !strings.HasPrefix(text, "\x01") {
442 return "", "", false
443 }
444 text = strings.Trim(text, "\x01")
445
446 words := strings.SplitN(text, " ", 2)
447 cmd = strings.ToUpper(words[0])
448 if len(words) > 1 {
449 params = words[1]
450 }
451
452 return cmd, params, true
453}
[478]454
455type casemapping func(string) string
456
457func casemapNone(name string) string {
458 return name
459}
460
461// CasemapASCII of name is the canonical representation of name according to the
462// ascii casemapping.
463func casemapASCII(name string) string {
[492]464 nameBytes := []byte(name)
465 for i, r := range nameBytes {
[478]466 if 'A' <= r && r <= 'Z' {
[492]467 nameBytes[i] = r + 'a' - 'A'
[478]468 }
469 }
[492]470 return string(nameBytes)
[478]471}
472
473// casemapRFC1459 of name is the canonical representation of name according to the
474// rfc1459 casemapping.
475func casemapRFC1459(name string) string {
[492]476 nameBytes := []byte(name)
477 for i, r := range nameBytes {
[478]478 if 'A' <= r && r <= 'Z' {
[492]479 nameBytes[i] = r + 'a' - 'A'
[478]480 } else if r == '{' {
[492]481 nameBytes[i] = '['
[478]482 } else if r == '}' {
[492]483 nameBytes[i] = ']'
[478]484 } else if r == '\\' {
[492]485 nameBytes[i] = '|'
[478]486 } else if r == '~' {
[492]487 nameBytes[i] = '^'
[478]488 }
489 }
[492]490 return string(nameBytes)
[478]491}
492
493// casemapRFC1459Strict of name is the canonical representation of name
494// according to the rfc1459-strict casemapping.
495func casemapRFC1459Strict(name string) string {
[492]496 nameBytes := []byte(name)
497 for i, r := range nameBytes {
[478]498 if 'A' <= r && r <= 'Z' {
[492]499 nameBytes[i] = r + 'a' - 'A'
[478]500 } else if r == '{' {
[492]501 nameBytes[i] = '['
[478]502 } else if r == '}' {
[492]503 nameBytes[i] = ']'
[478]504 } else if r == '\\' {
[492]505 nameBytes[i] = '|'
[478]506 }
507 }
[492]508 return string(nameBytes)
[478]509}
510
511func parseCasemappingToken(tokenValue string) (casemap casemapping, ok bool) {
512 switch tokenValue {
513 case "ascii":
514 casemap = casemapASCII
515 case "rfc1459":
516 casemap = casemapRFC1459
517 case "rfc1459-strict":
518 casemap = casemapRFC1459Strict
519 default:
520 return nil, false
521 }
522 return casemap, true
523}
524
525func partialCasemap(higher casemapping, name string) string {
[492]526 nameFullyCM := []byte(higher(name))
527 nameBytes := []byte(name)
528 for i, r := range nameBytes {
529 if !('A' <= r && r <= 'Z') && !('a' <= r && r <= 'z') {
530 nameBytes[i] = nameFullyCM[i]
[478]531 }
532 }
[492]533 return string(nameBytes)
[478]534}
535
536type casemapMap struct {
537 innerMap map[string]casemapEntry
538 casemap casemapping
539}
540
541type casemapEntry struct {
542 originalKey string
543 value interface{}
544}
545
546func newCasemapMap(size int) casemapMap {
547 return casemapMap{
548 innerMap: make(map[string]casemapEntry, size),
549 casemap: casemapNone,
550 }
551}
552
553func (cm *casemapMap) OriginalKey(name string) (key string, ok bool) {
554 entry, ok := cm.innerMap[cm.casemap(name)]
555 if !ok {
556 return "", false
557 }
558 return entry.originalKey, true
559}
560
561func (cm *casemapMap) Has(name string) bool {
562 _, ok := cm.innerMap[cm.casemap(name)]
563 return ok
564}
565
566func (cm *casemapMap) Len() int {
567 return len(cm.innerMap)
568}
569
570func (cm *casemapMap) SetValue(name string, value interface{}) {
571 nameCM := cm.casemap(name)
572 entry, ok := cm.innerMap[nameCM]
573 if !ok {
574 cm.innerMap[nameCM] = casemapEntry{
575 originalKey: name,
576 value: value,
577 }
578 return
579 }
580 entry.value = value
581 cm.innerMap[nameCM] = entry
582}
583
584func (cm *casemapMap) Delete(name string) {
585 delete(cm.innerMap, cm.casemap(name))
586}
587
588func (cm *casemapMap) SetCasemapping(newCasemap casemapping) {
589 cm.casemap = newCasemap
590 newInnerMap := make(map[string]casemapEntry, len(cm.innerMap))
591 for _, entry := range cm.innerMap {
592 newInnerMap[cm.casemap(entry.originalKey)] = entry
593 }
594 cm.innerMap = newInnerMap
595}
596
597type upstreamChannelCasemapMap struct{ casemapMap }
598
599func (cm *upstreamChannelCasemapMap) Value(name string) *upstreamChannel {
600 entry, ok := cm.innerMap[cm.casemap(name)]
601 if !ok {
602 return nil
603 }
604 return entry.value.(*upstreamChannel)
605}
606
607type channelCasemapMap struct{ casemapMap }
608
609func (cm *channelCasemapMap) Value(name string) *Channel {
610 entry, ok := cm.innerMap[cm.casemap(name)]
611 if !ok {
612 return nil
613 }
614 return entry.value.(*Channel)
615}
616
617type membershipsCasemapMap struct{ casemapMap }
618
619func (cm *membershipsCasemapMap) Value(name string) *memberships {
620 entry, ok := cm.innerMap[cm.casemap(name)]
621 if !ok {
622 return nil
623 }
624 return entry.value.(*memberships)
625}
626
[480]627type deliveredCasemapMap struct{ casemapMap }
[478]628
[480]629func (cm *deliveredCasemapMap) Value(name string) deliveredClientMap {
[478]630 entry, ok := cm.innerMap[cm.casemap(name)]
631 if !ok {
632 return nil
633 }
[480]634 return entry.value.(deliveredClientMap)
[478]635}
[498]636
637func isWordBoundary(r rune) bool {
638 switch r {
639 case '-', '_', '|':
640 return false
641 case '\u00A0':
642 return true
643 default:
644 return !unicode.IsLetter(r) && !unicode.IsNumber(r)
645 }
646}
647
648func isHighlight(text, nick string) bool {
649 for {
650 i := strings.Index(text, nick)
651 if i < 0 {
652 return false
653 }
654
655 // Detect word boundaries
656 var left, right rune
657 if i > 0 {
658 left, _ = utf8.DecodeLastRuneInString(text[:i])
659 }
660 if i < len(text) {
661 right, _ = utf8.DecodeRuneInString(text[i+len(nick):])
662 }
663 if isWordBoundary(left) && isWordBoundary(right) {
664 return true
665 }
666
667 text = text[i+len(nick):]
668 }
669}
[516]670
671// parseChatHistoryBound parses the given CHATHISTORY parameter as a bound.
672// The zero time is returned on error.
673func parseChatHistoryBound(param string) time.Time {
674 parts := strings.SplitN(param, "=", 2)
675 if len(parts) != 2 {
676 return time.Time{}
677 }
678 switch parts[0] {
679 case "timestamp":
680 timestamp, err := time.Parse(serverTimeLayout, parts[1])
681 if err != nil {
682 return time.Time{}
683 }
684 return timestamp
685 default:
686 return time.Time{}
687 }
688}
[660]689
690type whoxInfo struct {
691 Token string
692 Username string
693 Hostname string
694 Server string
695 Nickname string
696 Flags string
697 Account string
698 Realname string
699}
700
701func generateWHOXReply(prefix *irc.Prefix, nick, fields string, info *whoxInfo) *irc.Message {
702 if fields == "" {
703 return &irc.Message{
704 Prefix: prefix,
705 Command: irc.RPL_WHOREPLY,
706 Params: []string{nick, "*", info.Username, info.Hostname, info.Server, info.Nickname, info.Flags, "0 " + info.Realname},
707 }
708 }
709
710 fieldSet := make(map[byte]bool)
711 for i := 0; i < len(fields); i++ {
712 fieldSet[fields[i]] = true
713 }
714
715 var params []string
716 if fieldSet['t'] {
717 params = append(params, info.Token)
718 }
719 if fieldSet['c'] {
720 params = append(params, "*")
721 }
722 if fieldSet['u'] {
723 params = append(params, info.Username)
724 }
725 if fieldSet['i'] {
726 params = append(params, "255.255.255.255")
727 }
728 if fieldSet['h'] {
729 params = append(params, info.Hostname)
730 }
731 if fieldSet['s'] {
732 params = append(params, info.Server)
733 }
734 if fieldSet['n'] {
735 params = append(params, info.Nickname)
736 }
737 if fieldSet['f'] {
738 params = append(params, info.Flags)
739 }
740 if fieldSet['d'] {
741 params = append(params, "0")
742 }
743 if fieldSet['l'] { // idle time
744 params = append(params, "0")
745 }
746 if fieldSet['a'] {
747 account := "0" // WHOX uses "0" to mean "no account"
748 if info.Account != "" && info.Account != "*" {
749 account = info.Account
750 }
751 params = append(params, account)
752 }
753 if fieldSet['o'] {
754 params = append(params, "0")
755 }
756 if fieldSet['r'] {
757 params = append(params, info.Realname)
758 }
759
760 return &irc.Message{
761 Prefix: prefix,
762 Command: rpl_whospcrpl,
763 Params: append([]string{nick}, params...),
764 }
765}
[662]766
767var isupportEncoder = strings.NewReplacer(" ", "\\x20", "\\", "\\x5C")
768
769func encodeISUPPORT(s string) string {
770 return isupportEncoder.Replace(s)
771}
Note: See TracBrowser for help on using the repository browser.