initial message-parsing work

This commit is contained in:
David 2021-10-07 23:22:06 -04:00
commit 068ba8bfb0
4 changed files with 427 additions and 0 deletions

177
const.go Normal file
View 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"
)

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.yetaga.in/alazyreader/irc
go 1.17

137
message.go Normal file
View 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
View 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()
}
}
}