source: code/trunk/irc.go@ 802

Last change on this file since 802 was 786, checked in by contact, 3 years ago

irc: simplify isHighlight

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