1package irc
2
3import (
4 "errors"
5 "fmt"
6 "strings"
7 "time"
8)
9
10// CasemapASCII of name is the canonical representation of name according to the
11// ascii casemapping.
12func CasemapASCII(name string) string {
13 var sb strings.Builder
14 sb.Grow(len(name))
15 for _, r := range name {
16 if 'A' <= r && r <= 'Z' {
17 r += 'a' - 'A'
18 }
19 sb.WriteRune(r)
20 }
21 return sb.String()
22}
23
24// CasemapRFC1459 of name is the canonical representation of name according to the
25// rfc-1459 casemapping.
26func CasemapRFC1459(name string) string {
27 var sb strings.Builder
28 sb.Grow(len(name))
29 for _, r := range name {
30 if 'A' <= r && r <= 'Z' {
31 r += 'a' - 'A'
32 } else if r == '[' {
33 r = '{'
34 } else if r == ']' {
35 r = '}'
36 } else if r == '\\' {
37 r = '|'
38 } else if r == '~' {
39 r = '^'
40 }
41 sb.WriteRune(r)
42 }
43 return sb.String()
44}
45
46// word returns the first word of s and the rest of s.
47func word(s string) (word, rest string) {
48 split := strings.SplitN(s, " ", 2)
49 if len(split) < 2 {
50 word = split[0]
51 rest = ""
52 } else {
53 word = split[0]
54 rest = split[1]
55 }
56 return
57}
58
59// tagEscape returns the value of '\c' given c according to the message-tags
60// specification.
61func tagEscape(c rune) (escape rune) {
62 switch c {
63 case ':':
64 escape = ';'
65 case 's':
66 escape = ' '
67 case 'r':
68 escape = '\r'
69 case 'n':
70 escape = '\n'
71 default:
72 escape = c
73 }
74 return
75}
76
77// unescapeTagValue removes escapes from the given string and replaces them with
78// their meaningful values.
79func unescapeTagValue(escaped string) string {
80 var builder strings.Builder
81 builder.Grow(len(escaped))
82 escape := false
83
84 for _, c := range escaped {
85 if c == '\\' && !escape {
86 escape = true
87 } else {
88 var cpp rune
89
90 if escape {
91 cpp = tagEscape(c)
92 } else {
93 cpp = c
94 }
95
96 builder.WriteRune(cpp)
97 escape = false
98 }
99 }
100
101 return builder.String()
102}
103
104// escapeTagValue does the inverse operation of unescapeTagValue.
105func escapeTagValue(unescaped string) string {
106 var sb strings.Builder
107 sb.Grow(len(unescaped) * 2)
108
109 for _, c := range unescaped {
110 switch c {
111 case ';':
112 sb.WriteRune('\\')
113 sb.WriteRune(':')
114 case ' ':
115 sb.WriteRune('\\')
116 sb.WriteRune('s')
117 case '\r':
118 sb.WriteRune('\\')
119 sb.WriteRune('r')
120 case '\n':
121 sb.WriteRune('\\')
122 sb.WriteRune('n')
123 case '\\':
124 sb.WriteRune('\\')
125 sb.WriteRune('\\')
126 default:
127 sb.WriteRune(c)
128 }
129 }
130
131 return sb.String()
132}
133
134func parseTags(s string) (tags map[string]string) {
135 tags = map[string]string{}
136
137 for _, item := range strings.Split(s, ";") {
138 if item == "" || item == "=" || item == "+" || item == "+=" {
139 continue
140 }
141
142 kv := strings.SplitN(item, "=", 2)
143 if len(kv) < 2 {
144 tags[kv[0]] = ""
145 } else {
146 tags[kv[0]] = unescapeTagValue(kv[1])
147 }
148 }
149
150 return
151}
152
153func formatTags(tags map[string]string) string {
154 var sb strings.Builder
155 for k, v := range tags {
156 if sb.Len() > 0 {
157 sb.WriteRune(';')
158 }
159 sb.WriteString(k)
160 if v != "" {
161 sb.WriteRune('=')
162 sb.WriteString(escapeTagValue(v))
163 }
164 }
165 return sb.String()
166}
167
168var (
169 errEmptyMessage = errors.New("empty message")
170 errIncompleteMessage = errors.New("message is incomplete")
171)
172
173type Prefix struct {
174 Name string
175 User string
176 Host string
177}
178
179// ParsePrefix parses a "nick!user@host" combination (or a prefix) from the given
180// string.
181func ParsePrefix(s string) (p *Prefix) {
182 if s == "" {
183 return
184 }
185
186 p = &Prefix{}
187
188 spl0 := strings.Split(s, "@")
189 if 1 < len(spl0) {
190 p.Host = spl0[1]
191 }
192
193 spl1 := strings.Split(spl0[0], "!")
194 if 1 < len(spl1) {
195 p.User = spl1[1]
196 }
197
198 p.Name = spl1[0]
199
200 return
201}
202
203// Copy makes a copy of the prefix, but doesn't copy the internal strings.
204func (p *Prefix) Copy() *Prefix {
205 if p == nil {
206 return nil
207 }
208 res := &Prefix{}
209 *res = *p
210 return res
211}
212
213// String returns the "nick!user@host" representation of the prefix.
214func (p *Prefix) String() string {
215 if p == nil {
216 return ""
217 }
218
219 if p.User != "" && p.Host != "" {
220 return p.Name + "!" + p.User + "@" + p.Host
221 } else if p.User != "" {
222 return p.Name + "!" + p.User
223 } else if p.Host != "" {
224 return p.Name + "@" + p.Host
225 } else {
226 return p.Name
227 }
228}
229
230// Message is the representation of an IRC message.
231type Message struct {
232 Tags map[string]string
233 Prefix *Prefix
234 Command string
235 Params []string
236}
237
238func NewMessage(command string, params ...string) Message {
239 return Message{Command: command, Params: params}
240}
241
242// ParseMessage parses the message from the given string, which must be trimmed
243// of "\r\n" beforehand.
244func ParseMessage(line string) (msg Message, err error) {
245 line = strings.TrimLeft(line, " ")
246 if line == "" {
247 err = errEmptyMessage
248 return
249 }
250
251 if line[0] == '@' {
252 var tags string
253
254 tags, line = word(line)
255 msg.Tags = parseTags(tags[1:])
256 }
257
258 line = strings.TrimLeft(line, " ")
259 if line == "" {
260 err = errIncompleteMessage
261 return
262 }
263
264 if line[0] == ':' {
265 var prefix string
266
267 prefix, line = word(line)
268 msg.Prefix = ParsePrefix(prefix[1:])
269 }
270
271 line = strings.TrimLeft(line, " ")
272 if line == "" {
273 err = errIncompleteMessage
274 return
275 }
276
277 msg.Command, line = word(line)
278 msg.Command = strings.ToUpper(msg.Command)
279
280 msg.Params = make([]string, 0, 15)
281 for line != "" {
282 if line[0] == ':' {
283 msg.Params = append(msg.Params, line[1:])
284 break
285 }
286
287 var param string
288 param, line = word(line)
289 msg.Params = append(msg.Params, param)
290 }
291
292 return
293}
294
295func (msg Message) WithTag(key, value string) Message {
296 if msg.Tags == nil {
297 msg.Tags = map[string]string{}
298 }
299 msg.Tags[key] = value
300 return msg
301}
302
303// IsReply reports whether the message command is a server reply.
304func (msg *Message) IsReply() bool {
305 if len(msg.Command) != 3 {
306 return false
307 }
308 for _, r := range msg.Command {
309 if !('0' <= r && r <= '9') {
310 return false
311 }
312 }
313 return true
314}
315
316// String returns the protocol representation of the message, without an ending
317// "\r\n".
318func (msg *Message) String() string {
319 var sb strings.Builder
320
321 if msg.Tags != nil {
322 sb.WriteRune('@')
323 sb.WriteString(formatTags(msg.Tags))
324 sb.WriteRune(' ')
325 }
326
327 if msg.Prefix != nil {
328 sb.WriteRune(':')
329 sb.WriteString(msg.Prefix.String())
330 sb.WriteRune(' ')
331 }
332
333 sb.WriteString(msg.Command)
334
335 if len(msg.Params) != 0 {
336 for _, p := range msg.Params[:len(msg.Params)-1] {
337 sb.WriteRune(' ')
338 sb.WriteString(p)
339 }
340 lastParam := msg.Params[len(msg.Params)-1]
341 if !strings.ContainsRune(lastParam, ' ') && !strings.HasPrefix(lastParam, ":") {
342 sb.WriteRune(' ')
343 sb.WriteString(lastParam)
344 } else {
345 sb.WriteRune(' ')
346 sb.WriteRune(':')
347 sb.WriteString(lastParam)
348 }
349 }
350
351 return sb.String()
352}
353
354func (msg *Message) errNotEnoughParams(expected int) error {
355 return fmt.Errorf("expected at least %d params, got %d", expected, len(msg.Params))
356}
357
358func (msg *Message) ParseParams(out ...*string) error {
359 if len(msg.Params) < len(out) {
360 return msg.errNotEnoughParams(len(out))
361 }
362 for i := range out {
363 if out[i] != nil {
364 *out[i] = msg.Params[i]
365 }
366 }
367 return nil
368}
369
370const serverTimeLayout = "2006-01-02T15:04:05.000Z"
371
372func parseTimestamp(timestamp string) (time.Time, bool) {
373 t, err := time.Parse(serverTimeLayout, timestamp)
374 if err != nil {
375 return time.Time{}, false
376 }
377 return t.UTC(), true
378}
379
380func (msg *Message) ID() string {
381 return msg.Tags["msgid"]
382}
383
384// ReplyTo returns the msgid of the message this one is a reply to, if any.
385func (msg *Message) ReplyTo() string {
386 if id, ok := msg.Tags["+draft/reply"]; ok {
387 return id
388 }
389 return msg.Tags["+reply"]
390}
391
392// Time returns the time when the message has been sent, if present.
393func (msg *Message) Time() (t time.Time, ok bool) {
394 tag, ok := msg.Tags["time"]
395 if !ok {
396 return time.Time{}, false
397 }
398 return parseTimestamp(tag)
399}
400
401// TimeOrNow returns the time when the message has been sent, or time.Now() if
402// absent.
403func (msg *Message) TimeOrNow() time.Time {
404 t, ok := msg.Time()
405 if ok {
406 return t
407 }
408 return time.Now().UTC()
409}
410
411// Severity is the severity of a server reply.
412type Severity int
413
414const (
415 SeverityNote Severity = iota
416 SeverityWarn
417 SeverityFail
418)
419
420// ReplySeverity returns the severity of a server reply.
421func ReplySeverity(reply string) Severity {
422 switch reply[0] {
423 case '4', '5':
424 if reply == "422" {
425 return SeverityNote
426 } else {
427 return SeverityFail
428 }
429 case '9':
430 switch reply[2] {
431 case '2', '4', '5', '6', '7':
432 return SeverityFail
433 default:
434 return SeverityNote
435 }
436 default:
437 return SeverityNote
438 }
439}
440
441// Cap is a capability token in "CAP" server responses.
442type Cap struct {
443 Name string
444 Value string
445 Enable bool
446}
447
448// ParseCaps parses the last argument (capability list) of "CAP LS/LIST/NEW/DEL"
449// server responses.
450func ParseCaps(caps string) (diff []Cap) {
451 for _, c := range strings.Split(caps, " ") {
452 if c == "" || c == "-" || c == "=" || c == "-=" {
453 continue
454 }
455
456 var item Cap
457
458 if strings.HasPrefix(c, "-") {
459 item.Enable = false
460 c = c[1:]
461 } else {
462 item.Enable = true
463 }
464
465 kv := strings.SplitN(c, "=", 2)
466 item.Name = strings.ToLower(kv[0])
467 if len(kv) > 1 {
468 item.Value = kv[1]
469 }
470
471 diff = append(diff, item)
472 }
473
474 return
475}
476
477// Member is a token in RPL_NAMREPLY's last parameter.
478type Member struct {
479 PowerLevel string
480 Name *Prefix
481 Away bool
482 Disconnected bool
483 Self bool // Added by senpai
484 LastActive time.Time
485}
486
487type members struct {
488 m []Member
489 prefixes string
490}
491
492func (m members) Len() int {
493 return len(m.m)
494}
495
496func (m members) Less(i, j int) bool {
497 var pi, pj int
498 if m.m[i].PowerLevel != "" {
499 pi = strings.IndexByte(m.prefixes, m.m[i].PowerLevel[0])
500 } else {
501 pi = len(m.prefixes)
502 }
503 if m.m[j].PowerLevel != "" {
504 pj = strings.IndexByte(m.prefixes, m.m[j].PowerLevel[0])
505 } else {
506 pj = len(m.prefixes)
507 }
508 if pi != pj {
509 return pi < pj
510 }
511 return strings.ToLower(m.m[i].Name.Name) < strings.ToLower(m.m[j].Name.Name)
512}
513
514func (m members) Swap(i, j int) {
515 m.m[i], m.m[j] = m.m[j], m.m[i]
516}
517
518// ParseNameReply parses the last parameter of RPL_NAMREPLY, according to the
519// membership prefixes of the server.
520func ParseNameReply(trailing string, prefixes string) (names []Member) {
521 for _, word := range strings.Split(trailing, " ") {
522 if word == "" {
523 continue
524 }
525
526 name := strings.TrimLeft(word, prefixes)
527 names = append(names, Member{
528 PowerLevel: word[:len(word)-len(name)],
529 Name: ParsePrefix(name),
530 })
531 }
532
533 return
534}
535
536// Mode types available in the CHANMODES 005 token.
537const (
538 ModeTypeA int = iota
539 ModeTypeB
540 ModeTypeC
541 ModeTypeD
542)
543
544type ModeChange struct {
545 Enable bool
546 Mode byte
547 Param string
548}
549
550// ParseChannelMode parses a MODE message for a channel, according to the
551// CHANMODES of the server.
552func ParseChannelMode(mode string, params []string, chanmodes [4]string, membershipModes string) ([]ModeChange, error) {
553 var changes []ModeChange
554 enable := true
555 paramIdx := 0
556 for i := 0; i < len(mode); i++ {
557 m := mode[i]
558 if m == '+' || m == '-' {
559 enable = m == '+'
560 continue
561 }
562 modeType := -1
563 for t := 0; t < 4; t++ {
564 if 0 <= strings.IndexByte(chanmodes[t], m) {
565 modeType = t
566 break
567 }
568 }
569 if 0 <= strings.IndexByte(membershipModes, m) {
570 modeType = ModeTypeB
571 } else if modeType == -1 {
572 return nil, fmt.Errorf("unknown mode %c", m)
573 }
574 // ref: https://modern.ircdocs.horse/#mode-message
575 if modeType == ModeTypeA || modeType == ModeTypeB || (enable && modeType == ModeTypeC) {
576 if len(params) <= paramIdx {
577 return nil, fmt.Errorf("missing mode params")
578 }
579 changes = append(changes, ModeChange{
580 Enable: enable,
581 Mode: m,
582 Param: params[paramIdx],
583 })
584 paramIdx++
585 } else {
586 changes = append(changes, ModeChange{
587 Enable: enable,
588 Mode: m,
589 })
590 }
591 }
592 return changes, nil
593}