package irc import ( "bytes" "errors" "strings" ) var tagDecodeSlashMap = map[rune]rune{ ':': ';', 's': ' ', '\\': '\\', 'r': '\r', 'n': '\n', } var tagEncodeMap = map[rune]string{ ';': "\\:", ' ': "\\s", '\\': "\\\\", '\r': "\\r", '\n': "\\n", } var ( // ErrZeroLengthMessage is returned when parsing if the input is // zero-length. ErrZeroLengthMessage = errors.New("irc: Cannot parse zero-length message") // ErrMissingDataAfterPrefix is returned when parsing if there is // no message data after the prefix. ErrMissingDataAfterPrefix = errors.New("irc: No message data after prefix") // ErrMissingDataAfterTags is returned when parsing if there is no // message data after the tags. ErrMissingDataAfterTags = errors.New("irc: No message data after tags") // ErrMissingCommand is returned when parsing if there is no // command in the parsed message. ErrMissingCommand = errors.New("irc: Missing message command") ) // TagValue represents the value of a tag. type TagValue string // ParseTagValue parses a TagValue from the connection. If you need to // set a TagValue, you probably want to just set the string itself, so // it will be encoded properly. func ParseTagValue(v string) TagValue { ret := &bytes.Buffer{} input := bytes.NewBufferString(v) for { c, _, err := input.ReadRune() if err != nil { break } if c == '\\' { c2, _, err := input.ReadRune() // If we got a backslash then the end of the tag value, we should // just ignore the backslash. if err != nil { break } if replacement, ok := tagDecodeSlashMap[c2]; ok { ret.WriteRune(replacement) } else { ret.WriteRune(c2) } } else { ret.WriteRune(c) } } return TagValue(ret.String()) } // Encode converts a TagValue to the format in the connection. func (v TagValue) Encode() string { ret := &bytes.Buffer{} for _, c := range v { if replacement, ok := tagEncodeMap[c]; ok { ret.WriteString(replacement) } else { ret.WriteRune(c) } } return ret.String() } // Tags represents the IRCv3 message tags. type Tags map[string]TagValue // ParseTags takes a tag string and parses it into a tag map. It will // always return a tag map, even if there are no valid tags. func ParseTags(line string) Tags { ret := Tags{} tags := strings.Split(line, ";") for _, tag := range tags { parts := strings.SplitN(tag, "=", 2) if len(parts) < 2 { ret[parts[0]] = "" continue } ret[parts[0]] = ParseTagValue(parts[1]) } return ret } // GetTag is a convenience method to look up a tag in the map. func (t Tags) GetTag(key string) (string, bool) { ret, ok := t[key] return string(ret), ok } // Copy will create a new copy of all IRC tags attached to this // message. func (t Tags) Copy() Tags { ret := Tags{} for k, v := range t { ret[k] = v } return ret } // String ensures this is stringable func (t Tags) String() string { buf := &bytes.Buffer{} for k, v := range t { buf.WriteByte(';') buf.WriteString(k) if v != "" { buf.WriteByte('=') buf.WriteString(v.Encode()) } } // We don't need the first byte because that's an extra ';' // character. buf.ReadByte() return buf.String() } // Prefix represents the prefix of a message, generally the user who sent it type Prefix struct { // Name will contain the nick of who sent the message, the // server who sent the message, or a blank string Name string // User will either contain the user who sent the message or a blank string User string // Host will either contain the host of who sent the message or a blank string Host string } // ParsePrefix takes an identity string and parses it into an // identity struct. It will always return an Prefix struct and never // nil. func ParsePrefix(line string) *Prefix { // Start by creating an Prefix with nothing but the host id := &Prefix{ Name: line, } uh := strings.SplitN(id.Name, "@", 2) if len(uh) == 2 { id.Name, id.Host = uh[0], uh[1] } nu := strings.SplitN(id.Name, "!", 2) if len(nu) == 2 { id.Name, id.User = nu[0], nu[1] } return id } // Copy will create a new copy of an Prefix func (p *Prefix) Copy() *Prefix { if p == nil { return nil } newPrefix := &Prefix{} *newPrefix = *p return newPrefix } // String ensures this is stringable func (p *Prefix) String() string { buf := &bytes.Buffer{} buf.WriteString(p.Name) if p.User != "" { buf.WriteString("!") buf.WriteString(p.User) } if p.Host != "" { buf.WriteString("@") buf.WriteString(p.Host) } return buf.String() } // Message represents a line parsed from the server type Message struct { // Each message can have IRCv3 tags Tags // Each message can have a Prefix *Prefix // Command is which command is being called. Command string // Params are all the arguments for the command. Params []string } // MustParseMessage calls ParseMessage and either returns the message // or panics if an error is returned. func MustParseMessage(line string) *Message { m, err := ParseMessage(line) if err != nil { panic(err.Error()) } return m } // ParseMessage takes a message string (usually a whole line) and // parses it into a Message struct. This will return nil in the case // of invalid messages. func ParseMessage(line string) (*Message, error) { // Trim the line and make sure we have data line = strings.TrimRight(line, "\r\n") if len(line) == 0 { return nil, ErrZeroLengthMessage } c := &Message{ Tags: Tags{}, Prefix: &Prefix{}, } if line[0] == '@' { split := strings.SplitN(line, " ", 2) if len(split) < 2 { return nil, ErrMissingDataAfterTags } c.Tags = ParseTags(split[0][1:]) line = split[1] } if line[0] == ':' { split := strings.SplitN(line, " ", 2) if len(split) < 2 { return nil, ErrMissingDataAfterPrefix } // Parse the identity, if there was one c.Prefix = ParsePrefix(split[0][1:]) line = split[1] } // Split out the trailing then the rest of the args. Because // we expect there to be at least one result as an arg (the // command) we don't need to special case the trailing arg and // can just attempt a split on " :" split := strings.SplitN(line, " :", 2) c.Params = strings.FieldsFunc(split[0], func(r rune) bool { return r == ' ' }) // If there are no args, we need to bail because we need at // least the command. if len(c.Params) == 0 { return nil, ErrMissingCommand } // If we had a trailing arg, append it to the other args if len(split) == 2 { c.Params = append(c.Params, split[1]) } // Because of how it's parsed, the Command will show up as the // first arg. c.Command = strings.ToUpper(c.Params[0]) c.Params = c.Params[1:] // If there are no params, set it to nil, to make writing tests and other // things simpler. if len(c.Params) == 0 { c.Params = nil } return c, nil } // Trailing returns the last argument in the Message or an empty string // if there are no args func (m *Message) Trailing() string { if len(m.Params) < 1 { return "" } return m.Params[len(m.Params)-1] } // Copy will create a new copy of an message func (m *Message) Copy() *Message { // Create a new message newMessage := &Message{} // Copy stuff from the old message *newMessage = *m // Copy any IRcv3 tags newMessage.Tags = m.Tags.Copy() // Copy the Prefix newMessage.Prefix = m.Prefix.Copy() // Copy the Params slice newMessage.Params = append(make([]string, 0, len(m.Params)), m.Params...) // Similar to parsing, if Params is empty, set it to nil if len(newMessage.Params) == 0 { newMessage.Params = nil } return newMessage } // String ensures this is stringable func (m *Message) String() string { buf := &bytes.Buffer{} // Write any IRCv3 tags if they exist in the message if len(m.Tags) > 0 { buf.WriteByte('@') buf.WriteString(m.Tags.String()) buf.WriteByte(' ') } // Add the prefix if we have one if m.Prefix != nil && m.Prefix.Name != "" { buf.WriteByte(':') buf.WriteString(m.Prefix.String()) buf.WriteByte(' ') } // Add the command since we know we'll always have one buf.WriteString(m.Command) if len(m.Params) > 0 { args := m.Params[:len(m.Params)-1] trailing := m.Params[len(m.Params)-1] if len(args) > 0 { buf.WriteByte(' ') buf.WriteString(strings.Join(args, " ")) } // If trailing is zero-length, contains a space or starts with // a : we need to actually specify that it's trailing. if len(trailing) == 0 || strings.ContainsRune(trailing, ' ') || trailing[0] == ':' { buf.WriteString(" :") } else { buf.WriteString(" ") } buf.WriteString(trailing) } return buf.String() }