summaryrefslogtreecommitdiff
path: root/xS/irc.go
blob: 71bf961e7ad945ec16ec2497c8473fee632a613d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package main

import (
	"path/filepath"
	"regexp"
	"strings"
)

func ircToLower(c byte) byte {
	switch c {
	case '[':
		return '{'
	case ']':
		return '}'
	case '\\':
		return '|'
	case '~':
		return '^'
	}
	if c >= 'A' && c <= 'Z' {
		return c + ('a' - 'A')
	}
	return c
}

func ircToUpper(c byte) byte {
	switch c {
	case '{':
		return '['
	case '}':
		return ']'
	case '|':
		return '\\'
	case '^':
		return '~'
	}
	if c >= 'a' && c <= 'z' {
		return c - ('a' - 'A')
	}
	return c
}

// Convert identifier to a canonical form for case-insensitive comparisons.
// ircToUpper is used so that statically initialized maps can be in uppercase.
func ircToCanon(ident string) string {
	var canon []byte
	for _, c := range []byte(ident) {
		canon = append(canon, ircToUpper(c))
	}
	return string(canon)
}

func ircEqual(s1, s2 string) bool {
	return ircToCanon(s1) == ircToCanon(s2)
}

func ircFnmatch(pattern string, s string) bool {
	pattern, s = ircToCanon(pattern), ircToCanon(s)
	// FIXME: This should not support [] ranges and handle '/' specially.
	// We could translate the pattern to a regular expression.
	matched, _ := filepath.Match(pattern, s)
	return matched
}

var reMsg = regexp.MustCompile(
	`^(?:@([^ ]*) +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`)

type message struct {
	tags    map[string]string // IRC 3.2 message tags
	nick    string            // optional nickname
	user    string            // optional username
	host    string            // optional hostname or IP address
	command string            // command name
	params  []string          // arguments
}

func ircUnescapeMessageTag(value string) string {
	var buf []byte
	escape := false
	for i := 0; i < len(value); i++ {
		if escape {
			switch value[i] {
			case ':':
				buf = append(buf, ';')
			case 's':
				buf = append(buf, ' ')
			case 'r':
				buf = append(buf, '\r')
			case 'n':
				buf = append(buf, '\n')
			default:
				buf = append(buf, value[i])
			}
			escape = false
		} else if value[i] == '\\' {
			escape = true
		} else {
			buf = append(buf, value[i])
		}
	}
	return string(buf)
}

func ircParseMessageTags(tags string, out map[string]string) {
	for _, tag := range strings.Split(tags, ";") {
		if tag == "" {
			// Ignore empty.
		} else if equal := strings.IndexByte(tag, '='); equal < 0 {
			out[tag] = ""
		} else {
			out[tag[:equal]] = ircUnescapeMessageTag(tag[equal+1:])
		}
	}
}

func ircParseMessage(line string) *message {
	m := reMsg.FindStringSubmatch(line)
	if m == nil {
		return nil
	}

	msg := message{nil, m[2], m[3], m[4], m[5], nil}
	if m[1] != "" {
		msg.tags = make(map[string]string)
		ircParseMessageTags(m[1], msg.tags)
	}
	for _, x := range reArgs.FindAllString(m[6], -1) {
		msg.params = append(msg.params, x[1:])
	}
	return &msg
}