1 | package irc
|
---|
2 |
|
---|
3 | import (
|
---|
4 | "bytes"
|
---|
5 | "errors"
|
---|
6 | "strings"
|
---|
7 | )
|
---|
8 |
|
---|
9 | var tagDecodeSlashMap = map[rune]rune{
|
---|
10 | ':': ';',
|
---|
11 | 's': ' ',
|
---|
12 | '\\': '\\',
|
---|
13 | 'r': '\r',
|
---|
14 | 'n': '\n',
|
---|
15 | }
|
---|
16 |
|
---|
17 | var tagEncodeMap = map[rune]string{
|
---|
18 | ';': "\\:",
|
---|
19 | ' ': "\\s",
|
---|
20 | '\\': "\\\\",
|
---|
21 | '\r': "\\r",
|
---|
22 | '\n': "\\n",
|
---|
23 | }
|
---|
24 |
|
---|
25 | var (
|
---|
26 | // ErrZeroLengthMessage is returned when parsing if the input is
|
---|
27 | // zero-length.
|
---|
28 | ErrZeroLengthMessage = errors.New("irc: Cannot parse zero-length message")
|
---|
29 |
|
---|
30 | // ErrMissingDataAfterPrefix is returned when parsing if there is
|
---|
31 | // no message data after the prefix.
|
---|
32 | ErrMissingDataAfterPrefix = errors.New("irc: No message data after prefix")
|
---|
33 |
|
---|
34 | // ErrMissingDataAfterTags is returned when parsing if there is no
|
---|
35 | // message data after the tags.
|
---|
36 | ErrMissingDataAfterTags = errors.New("irc: No message data after tags")
|
---|
37 |
|
---|
38 | // ErrMissingCommand is returned when parsing if there is no
|
---|
39 | // command in the parsed message.
|
---|
40 | ErrMissingCommand = errors.New("irc: Missing message command")
|
---|
41 | )
|
---|
42 |
|
---|
43 | // TagValue represents the value of a tag.
|
---|
44 | type TagValue string
|
---|
45 |
|
---|
46 | // ParseTagValue parses a TagValue from the connection. If you need to
|
---|
47 | // set a TagValue, you probably want to just set the string itself, so
|
---|
48 | // it will be encoded properly.
|
---|
49 | func ParseTagValue(v string) TagValue {
|
---|
50 | ret := &bytes.Buffer{}
|
---|
51 |
|
---|
52 | input := bytes.NewBufferString(v)
|
---|
53 |
|
---|
54 | for {
|
---|
55 | c, _, err := input.ReadRune()
|
---|
56 | if err != nil {
|
---|
57 | break
|
---|
58 | }
|
---|
59 |
|
---|
60 | if c == '\\' {
|
---|
61 | c2, _, err := input.ReadRune()
|
---|
62 |
|
---|
63 | // If we got a backslash then the end of the tag value, we should
|
---|
64 | // just ignore the backslash.
|
---|
65 | if err != nil {
|
---|
66 | break
|
---|
67 | }
|
---|
68 |
|
---|
69 | if replacement, ok := tagDecodeSlashMap[c2]; ok {
|
---|
70 | ret.WriteRune(replacement)
|
---|
71 | } else {
|
---|
72 | ret.WriteRune(c2)
|
---|
73 | }
|
---|
74 | } else {
|
---|
75 | ret.WriteRune(c)
|
---|
76 | }
|
---|
77 | }
|
---|
78 |
|
---|
79 | return TagValue(ret.String())
|
---|
80 | }
|
---|
81 |
|
---|
82 | // Encode converts a TagValue to the format in the connection.
|
---|
83 | func (v TagValue) Encode() string {
|
---|
84 | ret := &bytes.Buffer{}
|
---|
85 |
|
---|
86 | for _, c := range v {
|
---|
87 | if replacement, ok := tagEncodeMap[c]; ok {
|
---|
88 | ret.WriteString(replacement)
|
---|
89 | } else {
|
---|
90 | ret.WriteRune(c)
|
---|
91 | }
|
---|
92 | }
|
---|
93 |
|
---|
94 | return ret.String()
|
---|
95 | }
|
---|
96 |
|
---|
97 | // Tags represents the IRCv3 message tags.
|
---|
98 | type Tags map[string]TagValue
|
---|
99 |
|
---|
100 | // ParseTags takes a tag string and parses it into a tag map. It will
|
---|
101 | // always return a tag map, even if there are no valid tags.
|
---|
102 | func ParseTags(line string) Tags {
|
---|
103 | ret := Tags{}
|
---|
104 |
|
---|
105 | tags := strings.Split(line, ";")
|
---|
106 | for _, tag := range tags {
|
---|
107 | parts := strings.SplitN(tag, "=", 2)
|
---|
108 | if len(parts) < 2 {
|
---|
109 | ret[parts[0]] = ""
|
---|
110 | continue
|
---|
111 | }
|
---|
112 |
|
---|
113 | ret[parts[0]] = ParseTagValue(parts[1])
|
---|
114 | }
|
---|
115 |
|
---|
116 | return ret
|
---|
117 | }
|
---|
118 |
|
---|
119 | // GetTag is a convenience method to look up a tag in the map.
|
---|
120 | func (t Tags) GetTag(key string) (string, bool) {
|
---|
121 | ret, ok := t[key]
|
---|
122 | return string(ret), ok
|
---|
123 | }
|
---|
124 |
|
---|
125 | // Copy will create a new copy of all IRC tags attached to this
|
---|
126 | // message.
|
---|
127 | func (t Tags) Copy() Tags {
|
---|
128 | ret := Tags{}
|
---|
129 |
|
---|
130 | for k, v := range t {
|
---|
131 | ret[k] = v
|
---|
132 | }
|
---|
133 |
|
---|
134 | return ret
|
---|
135 | }
|
---|
136 |
|
---|
137 | // String ensures this is stringable
|
---|
138 | func (t Tags) String() string {
|
---|
139 | buf := &bytes.Buffer{}
|
---|
140 |
|
---|
141 | for k, v := range t {
|
---|
142 | buf.WriteByte(';')
|
---|
143 | buf.WriteString(k)
|
---|
144 | if v != "" {
|
---|
145 | buf.WriteByte('=')
|
---|
146 | buf.WriteString(v.Encode())
|
---|
147 | }
|
---|
148 | }
|
---|
149 |
|
---|
150 | // We don't need the first byte because that's an extra ';'
|
---|
151 | // character.
|
---|
152 | buf.ReadByte()
|
---|
153 |
|
---|
154 | return buf.String()
|
---|
155 | }
|
---|
156 |
|
---|
157 | // Prefix represents the prefix of a message, generally the user who sent it
|
---|
158 | type Prefix struct {
|
---|
159 | // Name will contain the nick of who sent the message, the
|
---|
160 | // server who sent the message, or a blank string
|
---|
161 | Name string
|
---|
162 |
|
---|
163 | // User will either contain the user who sent the message or a blank string
|
---|
164 | User string
|
---|
165 |
|
---|
166 | // Host will either contain the host of who sent the message or a blank string
|
---|
167 | Host string
|
---|
168 | }
|
---|
169 |
|
---|
170 | // ParsePrefix takes an identity string and parses it into an
|
---|
171 | // identity struct. It will always return an Prefix struct and never
|
---|
172 | // nil.
|
---|
173 | func ParsePrefix(line string) *Prefix {
|
---|
174 | // Start by creating an Prefix with nothing but the host
|
---|
175 | id := &Prefix{
|
---|
176 | Name: line,
|
---|
177 | }
|
---|
178 |
|
---|
179 | uh := strings.SplitN(id.Name, "@", 2)
|
---|
180 | if len(uh) == 2 {
|
---|
181 | id.Name, id.Host = uh[0], uh[1]
|
---|
182 | }
|
---|
183 |
|
---|
184 | nu := strings.SplitN(id.Name, "!", 2)
|
---|
185 | if len(nu) == 2 {
|
---|
186 | id.Name, id.User = nu[0], nu[1]
|
---|
187 | }
|
---|
188 |
|
---|
189 | return id
|
---|
190 | }
|
---|
191 |
|
---|
192 | // Copy will create a new copy of an Prefix
|
---|
193 | func (p *Prefix) Copy() *Prefix {
|
---|
194 | if p == nil {
|
---|
195 | return nil
|
---|
196 | }
|
---|
197 |
|
---|
198 | newPrefix := &Prefix{}
|
---|
199 |
|
---|
200 | *newPrefix = *p
|
---|
201 |
|
---|
202 | return newPrefix
|
---|
203 | }
|
---|
204 |
|
---|
205 | // String ensures this is stringable
|
---|
206 | func (p *Prefix) String() string {
|
---|
207 | buf := &bytes.Buffer{}
|
---|
208 | buf.WriteString(p.Name)
|
---|
209 |
|
---|
210 | if p.User != "" {
|
---|
211 | buf.WriteString("!")
|
---|
212 | buf.WriteString(p.User)
|
---|
213 | }
|
---|
214 |
|
---|
215 | if p.Host != "" {
|
---|
216 | buf.WriteString("@")
|
---|
217 | buf.WriteString(p.Host)
|
---|
218 | }
|
---|
219 |
|
---|
220 | return buf.String()
|
---|
221 | }
|
---|
222 |
|
---|
223 | // Message represents a line parsed from the server
|
---|
224 | type Message struct {
|
---|
225 | // Each message can have IRCv3 tags
|
---|
226 | Tags
|
---|
227 |
|
---|
228 | // Each message can have a Prefix
|
---|
229 | *Prefix
|
---|
230 |
|
---|
231 | // Command is which command is being called.
|
---|
232 | Command string
|
---|
233 |
|
---|
234 | // Params are all the arguments for the command.
|
---|
235 | Params []string
|
---|
236 | }
|
---|
237 |
|
---|
238 | // MustParseMessage calls ParseMessage and either returns the message
|
---|
239 | // or panics if an error is returned.
|
---|
240 | func MustParseMessage(line string) *Message {
|
---|
241 | m, err := ParseMessage(line)
|
---|
242 | if err != nil {
|
---|
243 | panic(err.Error())
|
---|
244 | }
|
---|
245 | return m
|
---|
246 | }
|
---|
247 |
|
---|
248 | // ParseMessage takes a message string (usually a whole line) and
|
---|
249 | // parses it into a Message struct. This will return nil in the case
|
---|
250 | // of invalid messages.
|
---|
251 | func ParseMessage(line string) (*Message, error) {
|
---|
252 | // Trim the line and make sure we have data
|
---|
253 | line = strings.TrimRight(line, "\r\n")
|
---|
254 | if len(line) == 0 {
|
---|
255 | return nil, ErrZeroLengthMessage
|
---|
256 | }
|
---|
257 |
|
---|
258 | c := &Message{
|
---|
259 | Tags: Tags{},
|
---|
260 | Prefix: &Prefix{},
|
---|
261 | }
|
---|
262 |
|
---|
263 | if line[0] == '@' {
|
---|
264 | loc := strings.Index(line, " ")
|
---|
265 | if loc == -1 {
|
---|
266 | return nil, ErrMissingDataAfterTags
|
---|
267 | }
|
---|
268 |
|
---|
269 | c.Tags = ParseTags(line[1:loc])
|
---|
270 | line = line[loc+1:]
|
---|
271 | }
|
---|
272 |
|
---|
273 | if line[0] == ':' {
|
---|
274 | loc := strings.Index(line, " ")
|
---|
275 | if loc == -1 {
|
---|
276 | return nil, ErrMissingDataAfterPrefix
|
---|
277 | }
|
---|
278 |
|
---|
279 | // Parse the identity, if there was one
|
---|
280 | c.Prefix = ParsePrefix(line[1:loc])
|
---|
281 | line = line[loc+1:]
|
---|
282 | }
|
---|
283 |
|
---|
284 | // Split out the trailing then the rest of the args. Because
|
---|
285 | // we expect there to be at least one result as an arg (the
|
---|
286 | // command) we don't need to special case the trailing arg and
|
---|
287 | // can just attempt a split on " :"
|
---|
288 | split := strings.SplitN(line, " :", 2)
|
---|
289 | c.Params = strings.FieldsFunc(split[0], func(r rune) bool {
|
---|
290 | return r == ' '
|
---|
291 | })
|
---|
292 |
|
---|
293 | // If there are no args, we need to bail because we need at
|
---|
294 | // least the command.
|
---|
295 | if len(c.Params) == 0 {
|
---|
296 | return nil, ErrMissingCommand
|
---|
297 | }
|
---|
298 |
|
---|
299 | // If we had a trailing arg, append it to the other args
|
---|
300 | if len(split) == 2 {
|
---|
301 | c.Params = append(c.Params, split[1])
|
---|
302 | }
|
---|
303 |
|
---|
304 | // Because of how it's parsed, the Command will show up as the
|
---|
305 | // first arg.
|
---|
306 | c.Command = strings.ToUpper(c.Params[0])
|
---|
307 | c.Params = c.Params[1:]
|
---|
308 |
|
---|
309 | // If there are no params, set it to nil, to make writing tests and other
|
---|
310 | // things simpler.
|
---|
311 | if len(c.Params) == 0 {
|
---|
312 | c.Params = nil
|
---|
313 | }
|
---|
314 |
|
---|
315 | return c, nil
|
---|
316 | }
|
---|
317 |
|
---|
318 | // Param returns the i'th argument in the Message or an empty string
|
---|
319 | // if the requested arg does not exist
|
---|
320 | func (m *Message) Param(i int) string {
|
---|
321 | if i < 0 || i >= len(m.Params) {
|
---|
322 | return ""
|
---|
323 | }
|
---|
324 | return m.Params[i]
|
---|
325 | }
|
---|
326 |
|
---|
327 | // Trailing returns the last argument in the Message or an empty string
|
---|
328 | // if there are no args
|
---|
329 | func (m *Message) Trailing() string {
|
---|
330 | if len(m.Params) < 1 {
|
---|
331 | return ""
|
---|
332 | }
|
---|
333 |
|
---|
334 | return m.Params[len(m.Params)-1]
|
---|
335 | }
|
---|
336 |
|
---|
337 | // Copy will create a new copy of an message
|
---|
338 | func (m *Message) Copy() *Message {
|
---|
339 | // Create a new message
|
---|
340 | newMessage := &Message{}
|
---|
341 |
|
---|
342 | // Copy stuff from the old message
|
---|
343 | *newMessage = *m
|
---|
344 |
|
---|
345 | // Copy any IRcv3 tags
|
---|
346 | newMessage.Tags = m.Tags.Copy()
|
---|
347 |
|
---|
348 | // Copy the Prefix
|
---|
349 | newMessage.Prefix = m.Prefix.Copy()
|
---|
350 |
|
---|
351 | // Copy the Params slice
|
---|
352 | newMessage.Params = append(make([]string, 0, len(m.Params)), m.Params...)
|
---|
353 |
|
---|
354 | // Similar to parsing, if Params is empty, set it to nil
|
---|
355 | if len(newMessage.Params) == 0 {
|
---|
356 | newMessage.Params = nil
|
---|
357 | }
|
---|
358 |
|
---|
359 | return newMessage
|
---|
360 | }
|
---|
361 |
|
---|
362 | // String ensures this is stringable
|
---|
363 | func (m *Message) String() string {
|
---|
364 | buf := &bytes.Buffer{}
|
---|
365 |
|
---|
366 | // Write any IRCv3 tags if they exist in the message
|
---|
367 | if len(m.Tags) > 0 {
|
---|
368 | buf.WriteByte('@')
|
---|
369 | buf.WriteString(m.Tags.String())
|
---|
370 | buf.WriteByte(' ')
|
---|
371 | }
|
---|
372 |
|
---|
373 | // Add the prefix if we have one
|
---|
374 | if m.Prefix != nil && m.Prefix.Name != "" {
|
---|
375 | buf.WriteByte(':')
|
---|
376 | buf.WriteString(m.Prefix.String())
|
---|
377 | buf.WriteByte(' ')
|
---|
378 | }
|
---|
379 |
|
---|
380 | // Add the command since we know we'll always have one
|
---|
381 | buf.WriteString(m.Command)
|
---|
382 |
|
---|
383 | if len(m.Params) > 0 {
|
---|
384 | args := m.Params[:len(m.Params)-1]
|
---|
385 | trailing := m.Params[len(m.Params)-1]
|
---|
386 |
|
---|
387 | if len(args) > 0 {
|
---|
388 | buf.WriteByte(' ')
|
---|
389 | buf.WriteString(strings.Join(args, " "))
|
---|
390 | }
|
---|
391 |
|
---|
392 | // If trailing is zero-length, contains a space or starts with
|
---|
393 | // a : we need to actually specify that it's trailing.
|
---|
394 | if len(trailing) == 0 || strings.ContainsRune(trailing, ' ') || trailing[0] == ':' {
|
---|
395 | buf.WriteString(" :")
|
---|
396 | } else {
|
---|
397 | buf.WriteString(" ")
|
---|
398 | }
|
---|
399 | buf.WriteString(trailing)
|
---|
400 | }
|
---|
401 |
|
---|
402 | return buf.String()
|
---|
403 | }
|
---|