master sewn/kohai / irc / tokens.go
  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}