[822] | 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 | }
|
---|