source: code/trunk/irc.go@ 504

Last change on this file since 504 was 498, checked in by contact, 4 years ago

Move isHighlight to irc.go

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