initial message-parsing work
This commit is contained in:
commit
068ba8bfb0
177
const.go
Normal file
177
const.go
Normal file
@ -0,0 +1,177 @@
|
||||
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"
|
||||
)
|
137
message.go
Normal file
137
message.go
Normal file
@ -0,0 +1,137 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Tags map[string]string
|
||||
Source string
|
||||
Command command
|
||||
Parameters []string
|
||||
Raw string
|
||||
|
||||
parseIndex int
|
||||
}
|
||||
|
||||
func (m *Message) ParseMessage() error {
|
||||
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()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Message) parseTags() (map[string]string, error) {
|
||||
tags := make(map[string]string)
|
||||
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(";", " ")
|
||||
tags[key] = m.Raw[m.parseIndex : m.parseIndex+eot]
|
||||
m.parseIndex += len(tags[key]) + 1
|
||||
} else if m.Raw[m.parseIndex+next] == ';' {
|
||||
key := m.Raw[m.parseIndex : m.parseIndex+next]
|
||||
m.parseIndex += next
|
||||
tags[key] = ""
|
||||
m.parseIndex++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (m *Message) parseSource() (string, error) {
|
||||
start := m.parseIndex
|
||||
endofparse := strings.Index(m.Raw[m.parseIndex:], " ")
|
||||
if endofparse == -1 {
|
||||
return "", fmt.Errorf("end of string encountered while parsing tags")
|
||||
}
|
||||
m.parseIndex += endofparse
|
||||
return m.Raw[start:m.parseIndex], nil
|
||||
}
|
||||
|
||||
func (m *Message) parseCommand() (command, error) {
|
||||
start := m.parseIndex
|
||||
endofparse := strings.Index(m.Raw[m.parseIndex:], " ")
|
||||
if endofparse == -1 {
|
||||
return "", fmt.Errorf("end of string encountered while parsing command")
|
||||
}
|
||||
m.parseIndex += endofparse
|
||||
return command(m.Raw[start:m.parseIndex]), nil
|
||||
}
|
||||
|
||||
func (m *Message) parseParameters() ([]string, error) {
|
||||
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 after this one
|
||||
p := strings.TrimSuffix(m.Raw[m.parseIndex:], " ")
|
||||
if len(p) > 0 { // reject empty trailing params
|
||||
params = append(params, p)
|
||||
}
|
||||
break
|
||||
}
|
||||
params = append(params, m.Raw[m.parseIndex:m.parseIndex+endofparse])
|
||||
m.parseIndex += endofparse
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
if len(bs) == 0 {
|
||||
return 0
|
||||
}
|
||||
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 (m *Message) ToBytes() []byte {
|
||||
return nil
|
||||
}
|
110
message_test.go
Normal file
110
message_test.go
Normal file
@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
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: map[string]string{"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: map[string]string{"a": "b", "c": "32", "k": "", "rt": "ql7"},
|
||||
Source: "dan!d@localhost",
|
||||
Command: PRIVMSG,
|
||||
Parameters: []string{"#chan", "Hey what's up!"},
|
||||
}},
|
||||
{
|
||||
input: `@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= :dan!d@localhost PRIVMSG #chan :Hey what's up!`,
|
||||
output: &Message{
|
||||
Tags: map[string]string{"tag1": "value1", "tag2": "", "vendor1/tag3": "value2", "vendor2/tag4": ""},
|
||||
Source: "dan!d@localhost",
|
||||
Command: PRIVMSG,
|
||||
Parameters: []string{"#chan", "Hey what's up!"},
|
||||
}},
|
||||
{
|
||||
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 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"},
|
||||
}},
|
||||
}
|
||||
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()
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user