move message into its own package for now
This commit is contained in:
parent
3d37fd6d9e
commit
21a6917e31
177
const.go
177
const.go
@ -1,177 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
type command string
|
|
||||||
|
|
||||||
type channelType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
REGULAR channelType = iota
|
|
||||||
LOCAL
|
|
||||||
)
|
|
||||||
|
|
||||||
// client commands
|
|
||||||
const (
|
|
||||||
CAP command = "CAP"
|
|
||||||
AUTHENTICATE command = "AUTHENTICATE"
|
|
||||||
PASS command = "PASS"
|
|
||||||
NICK command = "NICK"
|
|
||||||
USER command = "USER"
|
|
||||||
OPER command = "OPER"
|
|
||||||
QUIT command = "QUIT"
|
|
||||||
|
|
||||||
JOIN command = "JOIN"
|
|
||||||
PART command = "PART"
|
|
||||||
TOPIC command = "TOPIC"
|
|
||||||
NAMES command = "NAMES"
|
|
||||||
LIST command = "LIST"
|
|
||||||
INVITE command = "INVITE"
|
|
||||||
KICK command = "KICK"
|
|
||||||
|
|
||||||
MOTD command = "MOTD"
|
|
||||||
VERSION command = "VERSION"
|
|
||||||
ADMIN command = "ADMIN"
|
|
||||||
CONNECT command = "CONNECT"
|
|
||||||
TIME command = "TIME"
|
|
||||||
STATS command = "STATS"
|
|
||||||
INFO command = "INFO"
|
|
||||||
MODE command = "MODE"
|
|
||||||
|
|
||||||
PRIVMSG command = "PRIVMSG"
|
|
||||||
NOTICE command = "NOTICE"
|
|
||||||
|
|
||||||
KILL command = "KILL"
|
|
||||||
|
|
||||||
AWAY command = "AWAY"
|
|
||||||
USERHOST command = "USERHOST"
|
|
||||||
)
|
|
||||||
|
|
||||||
// numerics
|
|
||||||
const (
|
|
||||||
RPL_WELCOME command = "001"
|
|
||||||
RPL_YOURHOST command = "002"
|
|
||||||
RPL_CREATED command = "003"
|
|
||||||
RPL_MYINFO command = "004"
|
|
||||||
RPL_ISUPPORT command = "005"
|
|
||||||
|
|
||||||
RPL_BOUNCE command = "010"
|
|
||||||
|
|
||||||
RPL_UMODEIS command = "221"
|
|
||||||
RPL_LUSERCLIENT command = "251"
|
|
||||||
RPL_LUSEROP command = "252"
|
|
||||||
RPL_LUSERUNKNOWN command = "253"
|
|
||||||
RPL_LUSERCHANNELS command = "254"
|
|
||||||
RPL_LUSERME command = "255"
|
|
||||||
RPL_ADMINME command = "256"
|
|
||||||
RPL_ADMINLOC1 command = "257"
|
|
||||||
RPL_ADMINLOC2 command = "258"
|
|
||||||
RPL_ADMINEMAIL command = "259"
|
|
||||||
|
|
||||||
RPL_TRYAGAIN command = "263"
|
|
||||||
|
|
||||||
RPL_LOCALUSERS command = "265"
|
|
||||||
RPL_GLOBALUSERS command = "266"
|
|
||||||
|
|
||||||
RPL_WHOISCERTFP command = "276"
|
|
||||||
|
|
||||||
RPL_NONE command = "300"
|
|
||||||
|
|
||||||
RPL_AWAY command = "301"
|
|
||||||
RPL_USERHOST command = "302"
|
|
||||||
RPL_ISON command = "303"
|
|
||||||
RPL_UNAWAY command = "305"
|
|
||||||
RPL_NOWAWAY command = "306"
|
|
||||||
|
|
||||||
RPL_WHOISUSER command = "311"
|
|
||||||
RPL_WHOISSERVER command = "312"
|
|
||||||
RPL_WHOISOPERATOR command = "313"
|
|
||||||
RPL_WHOWASUSER command = "314"
|
|
||||||
RPL_WHOISIDLE command = "317"
|
|
||||||
RPL_ENDOFWHOIS command = "318"
|
|
||||||
RPL_WHOISCHANNELS command = "319"
|
|
||||||
|
|
||||||
RPL_LISTSTART command = "321"
|
|
||||||
RPL_LIST command = "322"
|
|
||||||
RPL_LISTEND command = "323"
|
|
||||||
|
|
||||||
RPL_CHANNELMODEIS command = "324"
|
|
||||||
RPL_CREATIONTIME command = "329"
|
|
||||||
RPL_NOTOPIC command = "331"
|
|
||||||
RPL_TOPIC command = "332"
|
|
||||||
RPL_TOPICWHOTIME command = "333"
|
|
||||||
|
|
||||||
RPL_INVITING command = "341"
|
|
||||||
RPL_INVITELIST command = "346"
|
|
||||||
RPL_ENDOFINVITELIST command = "347"
|
|
||||||
RPL_EXCEPTLIST command = "348"
|
|
||||||
RPL_ENDOFEXCEPTLIST command = "349"
|
|
||||||
|
|
||||||
RPL_VERSION command = "351"
|
|
||||||
|
|
||||||
RPL_NAMREPLY command = "353"
|
|
||||||
RPL_ENDOFNAMES command = "366"
|
|
||||||
RPL_BANLIST command = "367"
|
|
||||||
RPL_ENDOFBANLIST command = "368"
|
|
||||||
RPL_ENDOFWHOWAS command = "369"
|
|
||||||
|
|
||||||
RPL_MOTD command = "372"
|
|
||||||
RPL_MOTDSTART command = "375"
|
|
||||||
RPL_ENDOFMOTD command = "376" // see also ERR_NOMOTD
|
|
||||||
|
|
||||||
RPL_YOUREOPER command = "381"
|
|
||||||
|
|
||||||
RPL_REHASHING command = "382"
|
|
||||||
|
|
||||||
ERR_UNKNOWNERROR command = "400"
|
|
||||||
ERR_NOSUCHNICK command = "401"
|
|
||||||
ERR_NOSUCHSERVER command = "402"
|
|
||||||
ERR_NOSUCHCHANNEL command = "403"
|
|
||||||
ERR_CANNOTSENDTOCHAN command = "404"
|
|
||||||
ERR_TOOMANYCHANNELS command = "405"
|
|
||||||
|
|
||||||
ERR_UNKNOWNCOMMAND command = "421"
|
|
||||||
|
|
||||||
ERR_NOMOTD command = "422"
|
|
||||||
|
|
||||||
ERR_ERRONEUSNICKNAME command = "432"
|
|
||||||
ERR_NICKNAMEINUSE command = "433"
|
|
||||||
|
|
||||||
ERR_USERNOTINCHANNEL command = "441"
|
|
||||||
ERR_NOTONCHANNEL command = "442"
|
|
||||||
ERR_USERONCHANNEL command = "443"
|
|
||||||
|
|
||||||
ERR_NOTREGISTERED command = "451"
|
|
||||||
|
|
||||||
ERR_NEEDMOREPARAMS command = "461"
|
|
||||||
ERR_ALREADYREGISTERED command = "462"
|
|
||||||
ERR_PASSWDMISMATCH command = "464"
|
|
||||||
ERR_YOUREBANNEDCREEP command = "465"
|
|
||||||
ERR_CHANNELISFULL command = "471"
|
|
||||||
ERR_UNKNOWNMODE command = "472"
|
|
||||||
ERR_INVITEONLYCHAN command = "473"
|
|
||||||
ERR_BANNEDFROMCHAN command = "474"
|
|
||||||
ERR_BADCHANNELKEY command = "475"
|
|
||||||
ERR_BADCHANMASK command = "476"
|
|
||||||
|
|
||||||
ERR_NOPRIVILEGES command = "481"
|
|
||||||
ERR_CHANOPRIVSNEEDED command = "482"
|
|
||||||
ERR_CANTKILLSERVER command = "483"
|
|
||||||
ERR_NOOPERHOST command = "491"
|
|
||||||
|
|
||||||
ERR_UMODEUNKNOWNFLAG command = "501"
|
|
||||||
ERR_USERSDONTMATCH command = "502"
|
|
||||||
|
|
||||||
RPL_STARTTLS command = "670"
|
|
||||||
ERR_STARTTLS command = "691"
|
|
||||||
|
|
||||||
ERR_NOPRIVS command = "723"
|
|
||||||
|
|
||||||
RPL_LOGGEDIN command = "900"
|
|
||||||
RPL_LOGGEDOUT command = "901"
|
|
||||||
ERR_NICKLOCKED command = "902"
|
|
||||||
RPL_SASLSUCCESS command = "903"
|
|
||||||
ERR_SASLFAIL command = "904"
|
|
||||||
ERR_SASLTOOLONG command = "905"
|
|
||||||
ERR_SASLABORTED command = "906"
|
|
||||||
ERR_SASLALREADY command = "907"
|
|
||||||
RPL_SASLMECHS command = "908"
|
|
||||||
)
|
|
170
message/const.go
Normal file
170
message/const.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
type Command string
|
||||||
|
|
||||||
|
// client commands
|
||||||
|
const (
|
||||||
|
CAP Command = "CAP"
|
||||||
|
AUTHENTICATE Command = "AUTHENTICATE"
|
||||||
|
PASS Command = "PASS"
|
||||||
|
NICK Command = "NICK"
|
||||||
|
USER Command = "USER"
|
||||||
|
OPER Command = "OPER"
|
||||||
|
QUIT Command = "QUIT"
|
||||||
|
|
||||||
|
JOIN Command = "JOIN"
|
||||||
|
PART Command = "PART"
|
||||||
|
TOPIC Command = "TOPIC"
|
||||||
|
NAMES Command = "NAMES"
|
||||||
|
LIST Command = "LIST"
|
||||||
|
INVITE Command = "INVITE"
|
||||||
|
KICK Command = "KICK"
|
||||||
|
|
||||||
|
MOTD Command = "MOTD"
|
||||||
|
VERSION Command = "VERSION"
|
||||||
|
ADMIN Command = "ADMIN"
|
||||||
|
CONNECT Command = "CONNECT"
|
||||||
|
TIME Command = "TIME"
|
||||||
|
STATS Command = "STATS"
|
||||||
|
INFO Command = "INFO"
|
||||||
|
MODE Command = "MODE"
|
||||||
|
|
||||||
|
PRIVMSG Command = "PRIVMSG"
|
||||||
|
NOTICE Command = "NOTICE"
|
||||||
|
|
||||||
|
KILL Command = "KILL"
|
||||||
|
|
||||||
|
AWAY Command = "AWAY"
|
||||||
|
USERHOST Command = "USERHOST"
|
||||||
|
)
|
||||||
|
|
||||||
|
// numerics
|
||||||
|
const (
|
||||||
|
RPL_WELCOME Command = "001"
|
||||||
|
RPL_YOURHOST Command = "002"
|
||||||
|
RPL_CREATED Command = "003"
|
||||||
|
RPL_MYINFO Command = "004"
|
||||||
|
RPL_ISUPPORT Command = "005"
|
||||||
|
|
||||||
|
RPL_BOUNCE Command = "010"
|
||||||
|
|
||||||
|
RPL_UMODEIS Command = "221"
|
||||||
|
RPL_LUSERCLIENT Command = "251"
|
||||||
|
RPL_LUSEROP Command = "252"
|
||||||
|
RPL_LUSERUNKNOWN Command = "253"
|
||||||
|
RPL_LUSERCHANNELS Command = "254"
|
||||||
|
RPL_LUSERME Command = "255"
|
||||||
|
RPL_ADMINME Command = "256"
|
||||||
|
RPL_ADMINLOC1 Command = "257"
|
||||||
|
RPL_ADMINLOC2 Command = "258"
|
||||||
|
RPL_ADMINEMAIL Command = "259"
|
||||||
|
|
||||||
|
RPL_TRYAGAIN Command = "263"
|
||||||
|
|
||||||
|
RPL_LOCALUSERS Command = "265"
|
||||||
|
RPL_GLOBALUSERS Command = "266"
|
||||||
|
|
||||||
|
RPL_WHOISCERTFP Command = "276"
|
||||||
|
|
||||||
|
RPL_NONE Command = "300"
|
||||||
|
|
||||||
|
RPL_AWAY Command = "301"
|
||||||
|
RPL_USERHOST Command = "302"
|
||||||
|
RPL_ISON Command = "303"
|
||||||
|
RPL_UNAWAY Command = "305"
|
||||||
|
RPL_NOWAWAY Command = "306"
|
||||||
|
|
||||||
|
RPL_WHOISUSER Command = "311"
|
||||||
|
RPL_WHOISSERVER Command = "312"
|
||||||
|
RPL_WHOISOPERATOR Command = "313"
|
||||||
|
RPL_WHOWASUSER Command = "314"
|
||||||
|
RPL_WHOISIDLE Command = "317"
|
||||||
|
RPL_ENDOFWHOIS Command = "318"
|
||||||
|
RPL_WHOISCHANNELS Command = "319"
|
||||||
|
|
||||||
|
RPL_LISTSTART Command = "321"
|
||||||
|
RPL_LIST Command = "322"
|
||||||
|
RPL_LISTEND Command = "323"
|
||||||
|
|
||||||
|
RPL_CHANNELMODEIS Command = "324"
|
||||||
|
RPL_CREATIONTIME Command = "329"
|
||||||
|
RPL_NOTOPIC Command = "331"
|
||||||
|
RPL_TOPIC Command = "332"
|
||||||
|
RPL_TOPICWHOTIME Command = "333"
|
||||||
|
|
||||||
|
RPL_INVITING Command = "341"
|
||||||
|
RPL_INVITELIST Command = "346"
|
||||||
|
RPL_ENDOFINVITELIST Command = "347"
|
||||||
|
RPL_EXCEPTLIST Command = "348"
|
||||||
|
RPL_ENDOFEXCEPTLIST Command = "349"
|
||||||
|
|
||||||
|
RPL_VERSION Command = "351"
|
||||||
|
|
||||||
|
RPL_NAMREPLY Command = "353"
|
||||||
|
RPL_ENDOFNAMES Command = "366"
|
||||||
|
RPL_BANLIST Command = "367"
|
||||||
|
RPL_ENDOFBANLIST Command = "368"
|
||||||
|
RPL_ENDOFWHOWAS Command = "369"
|
||||||
|
|
||||||
|
RPL_MOTD Command = "372"
|
||||||
|
RPL_MOTDSTART Command = "375"
|
||||||
|
RPL_ENDOFMOTD Command = "376" // see also ERR_NOMOTD
|
||||||
|
|
||||||
|
RPL_YOUREOPER Command = "381"
|
||||||
|
|
||||||
|
RPL_REHASHING Command = "382"
|
||||||
|
|
||||||
|
ERR_UNKNOWNERROR Command = "400"
|
||||||
|
ERR_NOSUCHNICK Command = "401"
|
||||||
|
ERR_NOSUCHSERVER Command = "402"
|
||||||
|
ERR_NOSUCHCHANNEL Command = "403"
|
||||||
|
ERR_CANNOTSENDTOCHAN Command = "404"
|
||||||
|
ERR_TOOMANYCHANNELS Command = "405"
|
||||||
|
|
||||||
|
ERR_UNKNOWNCOMMAND Command = "421"
|
||||||
|
|
||||||
|
ERR_NOMOTD Command = "422"
|
||||||
|
|
||||||
|
ERR_ERRONEUSNICKNAME Command = "432"
|
||||||
|
ERR_NICKNAMEINUSE Command = "433"
|
||||||
|
|
||||||
|
ERR_USERNOTINCHANNEL Command = "441"
|
||||||
|
ERR_NOTONCHANNEL Command = "442"
|
||||||
|
ERR_USERONCHANNEL Command = "443"
|
||||||
|
|
||||||
|
ERR_NOTREGISTERED Command = "451"
|
||||||
|
|
||||||
|
ERR_NEEDMOREPARAMS Command = "461"
|
||||||
|
ERR_ALREADYREGISTERED Command = "462"
|
||||||
|
ERR_PASSWDMISMATCH Command = "464"
|
||||||
|
ERR_YOUREBANNEDCREEP Command = "465"
|
||||||
|
ERR_CHANNELISFULL Command = "471"
|
||||||
|
ERR_UNKNOWNMODE Command = "472"
|
||||||
|
ERR_INVITEONLYCHAN Command = "473"
|
||||||
|
ERR_BANNEDFROMCHAN Command = "474"
|
||||||
|
ERR_BADCHANNELKEY Command = "475"
|
||||||
|
ERR_BADCHANMASK Command = "476"
|
||||||
|
|
||||||
|
ERR_NOPRIVILEGES Command = "481"
|
||||||
|
ERR_CHANOPRIVSNEEDED Command = "482"
|
||||||
|
ERR_CANTKILLSERVER Command = "483"
|
||||||
|
ERR_NOOPERHOST Command = "491"
|
||||||
|
|
||||||
|
ERR_UMODEUNKNOWNFLAG Command = "501"
|
||||||
|
ERR_USERSDONTMATCH Command = "502"
|
||||||
|
|
||||||
|
RPL_STARTTLS Command = "670"
|
||||||
|
ERR_STARTTLS Command = "691"
|
||||||
|
|
||||||
|
ERR_NOPRIVS Command = "723"
|
||||||
|
|
||||||
|
RPL_LOGGEDIN Command = "900"
|
||||||
|
RPL_LOGGEDOUT Command = "901"
|
||||||
|
ERR_NICKLOCKED Command = "902"
|
||||||
|
RPL_SASLSUCCESS Command = "903"
|
||||||
|
ERR_SASLFAIL Command = "904"
|
||||||
|
ERR_SASLTOOLONG Command = "905"
|
||||||
|
ERR_SASLABORTED Command = "906"
|
||||||
|
ERR_SASLALREADY Command = "907"
|
||||||
|
RPL_SASLMECHS Command = "908"
|
||||||
|
)
|
273
message/message.go
Normal file
273
message/message.go
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Tag struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags is an key-order-preserving, last-insert-wins "set" of Tag{}s
|
||||||
|
type Tags struct {
|
||||||
|
kv map[string]int // quick lookups for inserts
|
||||||
|
tags []Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert adds the given tag to the set, overriding any existing tag with that Key
|
||||||
|
func (t *Tags) Insert(new Tag) {
|
||||||
|
if t.kv == nil {
|
||||||
|
t.kv = map[string]int{}
|
||||||
|
}
|
||||||
|
i, ok := t.kv[new.Key]
|
||||||
|
if ok {
|
||||||
|
t.tags[i] = new
|
||||||
|
} else {
|
||||||
|
t.kv[new.Key] = len(t.tags)
|
||||||
|
t.tags = append(t.tags, new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSlice returns a simple slice of tags from the set
|
||||||
|
func (t *Tags) GetSlice() []Tag {
|
||||||
|
return t.tags
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Message represents a parsed set of Tags/Source/Command/Parameters from a Raw string.
|
||||||
|
//
|
||||||
|
// Tags/Source/Command/Parameters are all set from the Raw after ParseMessage() is called.
|
||||||
|
// Calling ParseMessage multiple times (without changing Raw) should be a no-op.
|
||||||
|
//
|
||||||
|
// Canonicalize() returns Tags/Source/Command/Parameters transformed back into an RFC-compliant []byte.
|
||||||
|
//
|
||||||
|
// Reusing a message struct to parse multiple Raw messages is possible, but frowned upon.
|
||||||
|
//
|
||||||
|
// Tag values are automatically escaped during parsing.
|
||||||
|
type Message struct {
|
||||||
|
Tags []Tag // An ordered slice of Key=Value pairs
|
||||||
|
Source string // An optional string denoting the source of the message
|
||||||
|
Command Command // A command/numeric for the message
|
||||||
|
Parameters []string // Zero or more parameters for the Command.
|
||||||
|
Raw string
|
||||||
|
|
||||||
|
parseIndex int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseMessage will attempt to parse whatever it's given as an IRC Message,
|
||||||
|
// and populate Tags/Source/Command/Parameters with the results.
|
||||||
|
// Garbage in, garbage out, though. The parser tries its best, but doesn't aggressively
|
||||||
|
// validate messages.
|
||||||
|
//
|
||||||
|
// Tag values are automatically escaped during parsing, as needed.
|
||||||
|
func (m *Message) ParseMessage() {
|
||||||
|
m.parseIndex = 0
|
||||||
|
m.consumeSpaces()
|
||||||
|
if m.consume('@') {
|
||||||
|
m.Tags = m.parseTags()
|
||||||
|
}
|
||||||
|
m.consumeSpaces()
|
||||||
|
if m.consume(':') {
|
||||||
|
m.Source = m.parseSource()
|
||||||
|
}
|
||||||
|
m.consumeSpaces()
|
||||||
|
m.Command = m.parseCommand()
|
||||||
|
m.consumeSpaces()
|
||||||
|
m.Parameters = m.parseParameters()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) parseTags() []Tag {
|
||||||
|
tags := &Tags{}
|
||||||
|
for {
|
||||||
|
next := m.findNext("=", ";")
|
||||||
|
if next == -1 {
|
||||||
|
break
|
||||||
|
} else if m.Raw[m.parseIndex+next] == '=' {
|
||||||
|
key := m.Raw[m.parseIndex : m.parseIndex+next]
|
||||||
|
m.parseIndex += next + 1
|
||||||
|
eot := m.findNext(";", " ")
|
||||||
|
if eot == -1 { // ran out of message, probably
|
||||||
|
tags.Insert(Tag{Key: key, Value: unescapeTag(m.Raw[m.parseIndex:])})
|
||||||
|
m.parseIndex = len(m.Raw)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
tags.Insert(Tag{Key: key, Value: unescapeTag(m.Raw[m.parseIndex : m.parseIndex+eot])})
|
||||||
|
m.parseIndex += len(m.Raw[m.parseIndex:m.parseIndex+eot]) + 1
|
||||||
|
} else if m.Raw[m.parseIndex+next] == ';' {
|
||||||
|
key := m.Raw[m.parseIndex : m.parseIndex+next]
|
||||||
|
m.parseIndex += next + 1
|
||||||
|
tags.Insert(Tag{Key: key, Value: ""})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tags.GetSlice()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) parseSource() string {
|
||||||
|
start := m.parseIndex
|
||||||
|
endofparse := strings.Index(m.Raw[m.parseIndex:], " ")
|
||||||
|
if endofparse == -1 {
|
||||||
|
m.parseIndex = len(m.Raw) // out of message! which is weird.
|
||||||
|
} else {
|
||||||
|
m.parseIndex += endofparse
|
||||||
|
}
|
||||||
|
return m.Raw[start:m.parseIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) parseCommand() Command {
|
||||||
|
start := m.parseIndex
|
||||||
|
endofparse := strings.Index(m.Raw[m.parseIndex:], " ")
|
||||||
|
if endofparse == -1 {
|
||||||
|
m.parseIndex = len(m.Raw) // a command with no parameters
|
||||||
|
} else {
|
||||||
|
m.parseIndex += endofparse
|
||||||
|
}
|
||||||
|
return Command(m.Raw[start:m.parseIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) parseParameters() []string {
|
||||||
|
params := []string{}
|
||||||
|
for m.parseIndex <= len(m.Raw) {
|
||||||
|
m.consumeSpaces()
|
||||||
|
if m.consume(':') { // "run until end of line"
|
||||||
|
params = append(params, m.Raw[m.parseIndex:])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
endofparse := strings.Index(m.Raw[m.parseIndex:], " ")
|
||||||
|
if endofparse == -1 { // no further params
|
||||||
|
break
|
||||||
|
}
|
||||||
|
params = append(params, m.Raw[m.parseIndex:m.parseIndex+endofparse])
|
||||||
|
m.parseIndex += endofparse
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) consumeSpaces() {
|
||||||
|
if len(m.Raw) <= m.parseIndex {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for len(m.Raw) > m.parseIndex && m.Raw[m.parseIndex] == ' ' {
|
||||||
|
m.parseIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) consume(r byte) bool {
|
||||||
|
if len(m.Raw) <= m.parseIndex {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if m.Raw[m.parseIndex] == r {
|
||||||
|
m.parseIndex++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) findNext(bs ...string) int {
|
||||||
|
r := -1
|
||||||
|
for _, b := range bs {
|
||||||
|
i := strings.Index(m.Raw[m.parseIndex:], b)
|
||||||
|
if r == -1 || (i != -1 && i < r) {
|
||||||
|
r = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func unescapeTag(s string) string {
|
||||||
|
sb := strings.Builder{}
|
||||||
|
escaping := false
|
||||||
|
i := 0
|
||||||
|
for i < len(s) {
|
||||||
|
if escaping {
|
||||||
|
switch s[i] {
|
||||||
|
case ':':
|
||||||
|
sb.WriteByte(';')
|
||||||
|
case '\\':
|
||||||
|
sb.WriteByte('\\')
|
||||||
|
case 's':
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
case 'r':
|
||||||
|
sb.WriteByte('\r')
|
||||||
|
case 'n':
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
default:
|
||||||
|
sb.WriteByte(s[i])
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
escaping = false
|
||||||
|
} else if s[i] == '\\' {
|
||||||
|
if !escaping {
|
||||||
|
escaping = true
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sb.WriteByte(s[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeTag(s string) string {
|
||||||
|
sb := strings.Builder{}
|
||||||
|
for i := range s {
|
||||||
|
switch s[i] {
|
||||||
|
case ';':
|
||||||
|
sb.Write([]byte{'\\', ':'})
|
||||||
|
case ' ':
|
||||||
|
sb.Write([]byte{'\\', 's'})
|
||||||
|
case '\\':
|
||||||
|
sb.Write([]byte{'\\', '\\'})
|
||||||
|
case '\n':
|
||||||
|
sb.Write([]byte{'\\', 'n'})
|
||||||
|
case '\r':
|
||||||
|
sb.Write([]byte{'\\', 'r'})
|
||||||
|
default:
|
||||||
|
sb.WriteByte(s[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) Canonicalize() []byte {
|
||||||
|
sb := bytes.NewBuffer([]byte{})
|
||||||
|
if m.Tags != nil && len(m.Tags) > 0 {
|
||||||
|
sb.WriteByte('@')
|
||||||
|
for i := range m.Tags {
|
||||||
|
sb.WriteString(m.Tags[i].Key)
|
||||||
|
if m.Tags[i].Value != "" {
|
||||||
|
sb.WriteByte('=')
|
||||||
|
sb.WriteString(escapeTag(m.Tags[i].Value))
|
||||||
|
}
|
||||||
|
if i+1 != len(m.Tags) {
|
||||||
|
sb.WriteByte(';')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Source != "" {
|
||||||
|
sb.WriteByte(':')
|
||||||
|
sb.WriteString(m.Source)
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString(string(m.Command))
|
||||||
|
|
||||||
|
if m.Parameters != nil && len(m.Parameters) > 0 {
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
for i := range m.Parameters {
|
||||||
|
if i+1 == len(m.Parameters) && (strings.Contains(m.Parameters[i], " ") || m.Parameters[i] == "" || m.Parameters[i][0] == ':') {
|
||||||
|
sb.WriteByte(':')
|
||||||
|
}
|
||||||
|
sb.WriteString(m.Parameters[i])
|
||||||
|
if i+1 != len(m.Parameters) {
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.Bytes()
|
||||||
|
}
|
358
message/message_test.go
Normal file
358
message/message_test.go
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestParsing(t *testing.T) {
|
||||||
|
testcases := []struct {
|
||||||
|
input string
|
||||||
|
output *Message
|
||||||
|
}{
|
||||||
|
// these inputs are based on
|
||||||
|
// https://github.com/ircdocs/parser-tests/blob/master/tests/msg-split.yaml
|
||||||
|
// and https://modern.ircdocs.horse/#messages
|
||||||
|
{
|
||||||
|
input: `:irc.example.com CAP LS * :multi-prefix extended-join sasl`,
|
||||||
|
output: &Message{
|
||||||
|
Source: "irc.example.com",
|
||||||
|
Command: CAP,
|
||||||
|
Parameters: []string{"LS", "*", "multi-prefix extended-join sasl"},
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `@id=234AB :dan!d@localhost PRIVMSG #chan :Hey what's up!`,
|
||||||
|
output: &Message{
|
||||||
|
Tags: []Tag{{"id", "234AB"}},
|
||||||
|
Source: "dan!d@localhost",
|
||||||
|
Command: PRIVMSG,
|
||||||
|
Parameters: []string{"#chan", "Hey what's up!"},
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `@a=b;c=32;k;rt=ql7 :dan!d@localhost PRIVMSG #chan :Hey what's up!`,
|
||||||
|
output: &Message{
|
||||||
|
Tags: []Tag{{"a", "b"}, {"c", "32"}, {"k", ""}, {"rt", "ql7"}},
|
||||||
|
Source: "dan!d@localhost",
|
||||||
|
Command: PRIVMSG,
|
||||||
|
Parameters: []string{"#chan", "Hey what's up!"},
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `@a=b\\and\nk;c=72\s45;d=gh\:764 CAP`,
|
||||||
|
output: &Message{
|
||||||
|
Tags: []Tag{{"a", "b\\and\nk"}, {"c", "72 45"}, {"d", `gh;764`}},
|
||||||
|
Command: CAP,
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= :dan!d@localhost PRIVMSG #chan :Hey what's up!`,
|
||||||
|
output: &Message{
|
||||||
|
Tags: []Tag{{"tag1", "value1"}, {"tag2", ""}, {"vendor1/tag3", "value2"}, {"vendor2/tag4", ""}},
|
||||||
|
Source: "dan!d@localhost",
|
||||||
|
Command: PRIVMSG,
|
||||||
|
Parameters: []string{"#chan", "Hey what's up!"},
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: ":src AWAY",
|
||||||
|
output: &Message{
|
||||||
|
Source: "src",
|
||||||
|
Command: AWAY,
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: ":src AWAY ",
|
||||||
|
output: &Message{
|
||||||
|
Source: "src",
|
||||||
|
Command: AWAY,
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `CAP REQ :sasl`,
|
||||||
|
output: &Message{
|
||||||
|
Command: CAP,
|
||||||
|
Parameters: []string{"REQ", "sasl"},
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `CAP REQ :`,
|
||||||
|
output: &Message{
|
||||||
|
Command: CAP,
|
||||||
|
Parameters: []string{"REQ", ""},
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `CAP REQ ::asdf`,
|
||||||
|
output: &Message{
|
||||||
|
Command: CAP,
|
||||||
|
Parameters: []string{"REQ", ":asdf"},
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `CAP REQ : asdf qwer`,
|
||||||
|
output: &Message{
|
||||||
|
Command: CAP,
|
||||||
|
Parameters: []string{"REQ", " asdf qwer"},
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: ":coolguy!ag@net\x035w\x03ork.admin PRIVMSG foo :bar baz",
|
||||||
|
output: &Message{
|
||||||
|
Source: "coolguy!ag@net\x035w\x03ork.admin",
|
||||||
|
Command: PRIVMSG,
|
||||||
|
Parameters: []string{"foo", "bar baz"},
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `:coolguy PRIVMSG bar :lol :) `,
|
||||||
|
output: &Message{
|
||||||
|
Source: "coolguy",
|
||||||
|
Command: PRIVMSG,
|
||||||
|
Parameters: []string{"bar", "lol :) "},
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `:gravel.mozilla.org 432 #momo :Erroneous Nickname: Illegal characters`,
|
||||||
|
output: &Message{
|
||||||
|
Source: "gravel.mozilla.org",
|
||||||
|
Command: ERR_ERRONEUSNICKNAME,
|
||||||
|
Parameters: []string{"#momo", "Erroneous Nickname: Illegal characters"},
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `:gravel.mozilla.org MODE #tckk +n `,
|
||||||
|
output: &Message{
|
||||||
|
Source: "gravel.mozilla.org",
|
||||||
|
Command: MODE,
|
||||||
|
Parameters: []string{"#tckk", "+n"},
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: ":services.esper.net MODE #foo-bar +o foobar ",
|
||||||
|
output: &Message{
|
||||||
|
Source: "services.esper.net",
|
||||||
|
Command: MODE,
|
||||||
|
Parameters: []string{"#foo-bar", "+o", "foobar"},
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `@tag1=value\\ntest COMMAND`,
|
||||||
|
output: &Message{
|
||||||
|
Tags: []Tag{{"tag1", `value\ntest`}},
|
||||||
|
Command: Command("COMMAND"),
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `@tag1=value\1 COMMAND`,
|
||||||
|
output: &Message{
|
||||||
|
Tags: []Tag{{"tag1", `value1`}},
|
||||||
|
Command: Command("COMMAND"),
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `@tag1=value\ COMMAND`,
|
||||||
|
output: &Message{
|
||||||
|
Tags: []Tag{{"tag1", `value`}},
|
||||||
|
Command: Command("COMMAND"),
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `@tag1=va\r\nlue COMMAND`,
|
||||||
|
output: &Message{
|
||||||
|
Tags: []Tag{{"tag1", "va\r\nlue"}},
|
||||||
|
Command: Command("COMMAND"),
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND`,
|
||||||
|
output: &Message{
|
||||||
|
Tags: []Tag{{"tag1", "5"}, {"tag2", "3"}, {"tag3", "4"}},
|
||||||
|
Command: Command("COMMAND"),
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND`,
|
||||||
|
output: &Message{
|
||||||
|
Tags: []Tag{{"tag1", "5"}, {"tag2", "3"}, {"tag3", "4"}, {"vendor/tag2", "8"}},
|
||||||
|
Command: Command("COMMAND"),
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `:SomeOp MODE #channel :+i`,
|
||||||
|
output: &Message{
|
||||||
|
Source: "SomeOp",
|
||||||
|
Command: MODE,
|
||||||
|
Parameters: []string{"#channel", "+i"},
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `:SomeOp MODE #channel +oo SomeUser :AnotherUser`,
|
||||||
|
output: &Message{
|
||||||
|
Source: "SomeOp",
|
||||||
|
Command: MODE,
|
||||||
|
Parameters: []string{"#channel", "+oo", "SomeUser", "AnotherUser"},
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
input: `:uttergarbage`,
|
||||||
|
output: &Message{
|
||||||
|
Source: "uttergarbage",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `@only=tags`,
|
||||||
|
output: &Message{
|
||||||
|
Tags: []Tag{{"only", "tags"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `@only=`,
|
||||||
|
output: &Message{
|
||||||
|
Tags: []Tag{{"only", ""}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testcases {
|
||||||
|
m := &Message{Raw: tc.input}
|
||||||
|
m.ParseMessage()
|
||||||
|
if len(tc.output.Tags) != len(m.Tags) {
|
||||||
|
t.Logf("tags: actual: %v, expected: %v", m.Tags, tc.output.Tags)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
for i := range tc.output.Tags {
|
||||||
|
if tc.output.Tags[i] != m.Tags[i] {
|
||||||
|
t.Logf("tags: actual: %+v, expected: %+v", m.Tags[i], tc.output.Tags[i])
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tc.output.Source != m.Source {
|
||||||
|
t.Logf("source: actual: %v, expected: %v", m.Source, tc.output.Source)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if tc.output.Command != m.Command {
|
||||||
|
t.Logf("command: actual: %v, expected: %v", m.Command, tc.output.Command)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if len(tc.output.Parameters) != len(m.Parameters) {
|
||||||
|
t.Logf("parameters: actual: %v (%d), expected: %v (%d)", m.Parameters, len(m.Parameters), tc.output.Parameters, len(tc.output.Parameters))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
for i := range tc.output.Parameters {
|
||||||
|
if tc.output.Parameters[i] != m.Parameters[i] {
|
||||||
|
t.Logf("parameters: actual: %v, expected: %v", m.Parameters[i], tc.output.Parameters[i])
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanonicalization(t *testing.T) {
|
||||||
|
testcases := []struct {
|
||||||
|
input *Message
|
||||||
|
output string
|
||||||
|
}{
|
||||||
|
// these outputs are based on
|
||||||
|
// https://github.com/ircdocs/parser-tests/blob/master/tests/msg-split.yaml
|
||||||
|
// and https://modern.ircdocs.horse/#messages
|
||||||
|
{
|
||||||
|
input: &Message{
|
||||||
|
Source: "irc.example.com",
|
||||||
|
Command: CAP,
|
||||||
|
Parameters: []string{"LS", "*", "multi-prefix extended-join sasl"},
|
||||||
|
},
|
||||||
|
output: `:irc.example.com CAP LS * :multi-prefix extended-join sasl`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: &Message{
|
||||||
|
Tags: []Tag{{"id", "234AB"}},
|
||||||
|
Source: "dan!d@localhost",
|
||||||
|
Command: PRIVMSG,
|
||||||
|
Parameters: []string{"#chan", "Hey what's up!"},
|
||||||
|
},
|
||||||
|
output: `@id=234AB :dan!d@localhost PRIVMSG #chan :Hey what's up!`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: &Message{
|
||||||
|
Tags: []Tag{{"a", "b"}, {"c", "32"}, {"k", ""}, {"rt", "ql7"}},
|
||||||
|
Source: "dan!d@localhost",
|
||||||
|
Command: PRIVMSG,
|
||||||
|
Parameters: []string{"#chan", "Hey what's up!"},
|
||||||
|
},
|
||||||
|
output: `@a=b;c=32;k;rt=ql7 :dan!d@localhost PRIVMSG #chan :Hey what's up!`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: &Message{
|
||||||
|
Tags: []Tag{{"a", "b\\and\nk"}, {"c", "72 45"}, {"d", `gh;764`}},
|
||||||
|
Command: CAP,
|
||||||
|
},
|
||||||
|
output: `@a=b\\and\nk;c=72\s45;d=gh\:764 CAP`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: &Message{
|
||||||
|
Tags: []Tag{{"tag1", "value1"}, {"tag2", ""}, {"vendor1/tag3", "value2"}, {"vendor2/tag4", ""}},
|
||||||
|
Source: "dan!d@localhost",
|
||||||
|
Command: PRIVMSG,
|
||||||
|
Parameters: []string{"#chan", "Hey what's up!"},
|
||||||
|
},
|
||||||
|
output: `@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 :dan!d@localhost PRIVMSG #chan :Hey what's up!`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: &Message{
|
||||||
|
Source: "src",
|
||||||
|
Command: AWAY,
|
||||||
|
},
|
||||||
|
output: ":src AWAY",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: &Message{
|
||||||
|
Command: CAP,
|
||||||
|
Parameters: []string{"REQ", "sasl"},
|
||||||
|
},
|
||||||
|
output: `CAP REQ sasl`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: &Message{
|
||||||
|
Command: CAP,
|
||||||
|
Parameters: []string{"REQ", ""},
|
||||||
|
},
|
||||||
|
output: `CAP REQ :`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: &Message{
|
||||||
|
Command: CAP,
|
||||||
|
Parameters: []string{"REQ", ":asdf"},
|
||||||
|
},
|
||||||
|
output: `CAP REQ ::asdf`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: &Message{
|
||||||
|
Command: CAP,
|
||||||
|
Parameters: []string{"REQ", " asdf qwer"},
|
||||||
|
},
|
||||||
|
output: `CAP REQ : asdf qwer`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: &Message{
|
||||||
|
Source: "coolguy",
|
||||||
|
Command: PRIVMSG,
|
||||||
|
Parameters: []string{"bar", "lol :) "},
|
||||||
|
},
|
||||||
|
output: `:coolguy PRIVMSG bar :lol :) `,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: &Message{
|
||||||
|
Source: "coolguy!ag@net\x035w\x03ork.admin",
|
||||||
|
Command: PRIVMSG,
|
||||||
|
Parameters: []string{"foo", "bar baz"},
|
||||||
|
},
|
||||||
|
output: ":coolguy!ag@net\x035w\x03ork.admin PRIVMSG foo :bar baz",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: &Message{
|
||||||
|
Tags: []Tag{{"tag1", `value\ntest`}},
|
||||||
|
Command: Command("COMMAND"),
|
||||||
|
},
|
||||||
|
output: `@tag1=value\\ntest COMMAND`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: &Message{
|
||||||
|
Tags: []Tag{{"tag1", "va\r\nlue"}},
|
||||||
|
Command: Command("COMMAND"),
|
||||||
|
},
|
||||||
|
output: `@tag1=va\r\nlue COMMAND`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testcases {
|
||||||
|
output := string(tc.input.Canonicalize())
|
||||||
|
if output != tc.output {
|
||||||
|
t.Logf("received '%s', expected '%s'", output, tc.output)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoundTrip(t *testing.T) {
|
||||||
|
in := `@a=b;c=3\n2;k;rt=ql7 :dan!d@localhost PRIVMSG #chan :Hey what's up! `
|
||||||
|
m := &Message{Raw: in}
|
||||||
|
m.ParseMessage()
|
||||||
|
out := m.Canonicalize()
|
||||||
|
if string(out) != in {
|
||||||
|
t.Logf("expected '%s', received '%s'", in, string(out))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user