395 lines
8.4 KiB
Go
395 lines
8.4 KiB
Go
|
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()
|
||
|
}
|