Skip to content

Commit

Permalink
Merge pull request #67 from go-irc/client-cleanup
Browse files Browse the repository at this point in the history
Move client code out into separate filter functions
  • Loading branch information
belak committed Apr 11, 2018
2 parents 5bf07c6 + 3ec935b commit 29f1845
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 110 deletions.
90 changes: 0 additions & 90 deletions client.go
Expand Up @@ -5,100 +5,10 @@ import (
"errors"
"fmt"
"io"
"strings"
"sync"
"time"
)

// clientFilters are pre-processing which happens for certain message
// types. These were moved from below to keep the complexity of each
// component down.
var clientFilters = map[string]func(*Client, *Message){
"001": func(c *Client, m *Message) {
c.currentNick = m.Params[0]
c.connected = true
},
"433": func(c *Client, m *Message) {
// We only want to try and handle nick collisions during the initial
// handshake.
if c.connected {
return
}
c.currentNick = c.currentNick + "_"
c.Writef("NICK :%s", c.currentNick)
},
"437": func(c *Client, m *Message) {
// We only want to try and handle nick collisions during the initial
// handshake.
if c.connected {
return
}
c.currentNick = c.currentNick + "_"
c.Writef("NICK :%s", c.currentNick)
},
"PING": func(c *Client, m *Message) {
reply := m.Copy()
reply.Command = "PONG"
c.WriteMessage(reply)
},
"PONG": func(c *Client, m *Message) {
if c.incomingPongChan != nil {
select {
case c.incomingPongChan <- m.Trailing():
default:
}
}
},
"NICK": func(c *Client, m *Message) {
if m.Prefix.Name == c.currentNick && len(m.Params) > 0 {
c.currentNick = m.Params[0]
}
},
"CAP": func(c *Client, m *Message) {
if c.remainingCapResponses <= 0 || len(m.Params) <= 2 {
return
}

switch m.Params[1] {
case "LS":
for _, key := range strings.Split(m.Trailing(), " ") {
cap := c.caps[key]
cap.Available = true
c.caps[key] = cap
}
c.remainingCapResponses--
case "ACK":
for _, key := range strings.Split(m.Trailing(), " ") {
cap := c.caps[key]
cap.Enabled = true
c.caps[key] = cap
}
c.remainingCapResponses--
case "NAK":
// If we got a NAK and this REQ was required, we need to bail
// with an error.
for _, key := range strings.Split(m.Trailing(), " ") {
if c.caps[key].Required {
c.sendError(fmt.Errorf("CAP %s requested but was rejected", key))
return
}
}
c.remainingCapResponses--
}

if c.remainingCapResponses <= 0 {
for key, cap := range c.caps {
if cap.Required && !cap.Enabled {
c.sendError(fmt.Errorf("CAP %s requested but not accepted", key))
return
}
}

c.Write("CAP END")
}
},
}

// ClientConfig is a structure used to configure a Client.
type ClientConfig struct {
// General connection information.
Expand Down
151 changes: 151 additions & 0 deletions client_handlers.go
@@ -0,0 +1,151 @@
package irc

import (
"fmt"
"strings"
)

type clientFilter func(*Client, *Message)

// clientFilters are pre-processing which happens for certain message
// types. These were moved from below to keep the complexity of each
// component down.
var clientFilters = map[string]clientFilter{
"001": handle001,
"433": handle433,
"437": handle437,
"PING": handlePing,
"PONG": handlePong,
"NICK": handleNick,
"CAP": handleCap,
}

// From rfc2812 section 5.1 (Command responses)
//
// 001 RPL_WELCOME
// "Welcome to the Internet Relay Network
// <nick>!<user>@<host>"
func handle001(c *Client, m *Message) {
c.currentNick = m.Params[0]
c.connected = true
}

// From rfc2812 section 5.2 (Error Replies)
//
// 433 ERR_NICKNAMEINUSE
// "<nick> :Nickname is already in use"
//
// - Returned when a NICK message is processed that results
// in an attempt to change to a currently existing
// nickname.
func handle433(c *Client, m *Message) {
// We only want to try and handle nick collisions during the initial
// handshake.
if c.connected {
return
}
c.currentNick = c.currentNick + "_"
c.Writef("NICK :%s", c.currentNick)
}

// From rfc2812 section 5.2 (Error Replies)
//
// 437 ERR_UNAVAILRESOURCE
// "<nick/channel> :Nick/channel is temporarily unavailable"
//
// - Returned by a server to a user trying to join a channel
// currently blocked by the channel delay mechanism.
//
// - Returned by a server to a user trying to change nickname
// when the desired nickname is blocked by the nick delay
// mechanism.
func handle437(c *Client, m *Message) {
// We only want to try and handle nick collisions during the initial
// handshake.
if c.connected {
return
}
c.currentNick = c.currentNick + "_"
c.Writef("NICK :%s", c.currentNick)
}

func handlePing(c *Client, m *Message) {
reply := m.Copy()
reply.Command = "PONG"
c.WriteMessage(reply)
}

func handlePong(c *Client, m *Message) {
if c.incomingPongChan != nil {
select {
case c.incomingPongChan <- m.Trailing():
default:
// Note that this return isn't really needed, but it helps some code
// coverage tools actually see this line.
return
}
}
}

func handleNick(c *Client, m *Message) {
if m.Prefix.Name == c.currentNick && len(m.Params) > 0 {
c.currentNick = m.Params[0]
}
}

var capFilters = map[string]clientFilter{
"LS": handleCapLs,
"ACK": handleCapAck,
"NAK": handleCapNak,
}

func handleCap(c *Client, m *Message) {
if c.remainingCapResponses <= 0 || len(m.Params) <= 2 {
return
}

if filter, ok := capFilters[m.Params[1]]; ok {
filter(c, m)
}

if c.remainingCapResponses <= 0 {
for key, cap := range c.caps {
if cap.Required && !cap.Enabled {
c.sendError(fmt.Errorf("CAP %s requested but not accepted", key))
return
}
}

c.Write("CAP END")
}
}

func handleCapLs(c *Client, m *Message) {
for _, key := range strings.Split(m.Trailing(), " ") {
cap := c.caps[key]
cap.Available = true
c.caps[key] = cap
}
c.remainingCapResponses--
}

func handleCapAck(c *Client, m *Message) {
for _, key := range strings.Split(m.Trailing(), " ") {
cap := c.caps[key]
cap.Enabled = true
c.caps[key] = cap
}
c.remainingCapResponses--
}

func handleCapNak(c *Client, m *Message) {
// If we got a NAK and this REQ was required, we need to bail
// with an error.
for _, key := range strings.Split(m.Trailing(), " ") {
if c.caps[key].Required {
c.sendError(fmt.Errorf("CAP %s requested but was rejected", key))
return
}
}
c.remainingCapResponses--
}
14 changes: 4 additions & 10 deletions client_test.go
Expand Up @@ -388,20 +388,14 @@ func TestPingLoop(t *testing.T) {

// This one is just for coverage, so we know we're hitting the
// branch that drops extra pings.
runClientTest(t, config, io.EOF, nil, []TestAction{
runClientTest(t, config, io.EOF, func(c *Client) {
c.incomingPongChan = make(chan string)
handlePong(c, MustParseMessage("PONG :hello 1"))
}, []TestAction{
ExpectLine("PASS :test_pass\r\n"),
ExpectLine("NICK :test_nick\r\n"),
ExpectLine("USER test_user 0.0.0.0 0.0.0.0 :test_name\r\n"),
SendLine("001 :hello_world\r\n"),

// It's a buffered channel of 5, so we want to send at least 6 of them
SendLine("PONG :hello 1\r\n"),
SendLine("PONG :hello 2\r\n"),
SendLine("PONG :hello 3\r\n"),
SendLine("PONG :hello 4\r\n"),
SendLine("PONG :hello 5\r\n"),
SendLine("PONG :hello 6\r\n"),
SendLine("PONG :hello 7\r\n"),
})

// Successful ping with write error
Expand Down
22 changes: 13 additions & 9 deletions parser_test.go
Expand Up @@ -113,6 +113,7 @@ func TestMessageCopy(t *testing.T) {

type MsgSplitTests struct {
Tests []struct {
Desc string
Input string
Atoms struct {
Source *string
Expand All @@ -135,15 +136,15 @@ func TestMsgSplit(t *testing.T) {

for _, test := range splitTests.Tests {
msg, err := ParseMessage(test.Input)
assert.NoError(t, err, "Failed to parse: %s (%s)", test.Input, err)
assert.NoError(t, err, "%s: Failed to parse: %s (%s)", test.Desc, test.Input, err)

assert.Equal(t,
strings.ToUpper(test.Atoms.Verb), msg.Command,
"Wrong command for input: %s", test.Input,
"%s: Wrong command for input: %s", test.Desc, test.Input,
)
assert.Equal(t,
test.Atoms.Params, msg.Params,
"Wrong params for input: %s", test.Input,
"%s: Wrong params for input: %s", test.Desc, test.Input,
)

if test.Atoms.Source != nil {
Expand All @@ -152,23 +153,25 @@ func TestMsgSplit(t *testing.T) {

assert.Equal(t,
len(test.Atoms.Tags), len(msg.Tags),
"Wrong number of tags",
"%s: Wrong number of tags",
test.Desc,
)

for k, v := range test.Atoms.Tags {
tag, ok := msg.GetTag(k)
assert.True(t, ok, "Missing tag")
if v == nil {
assert.EqualValues(t, "", tag, "Tag differs")
assert.EqualValues(t, "", tag, "%s: Tag %q differs: %s != \"\"", test.Desc, k, tag)
} else {
assert.EqualValues(t, v, tag, "Tag differs")
assert.EqualValues(t, v, tag, "%s: Tag %q differs: %s != %s", test.Desc, k, v, tag)
}
}
}
}

type MsgJoinTests struct {
Tests []struct {
Desc string
Atoms struct {
Source string
Verb string
Expand Down Expand Up @@ -211,6 +214,7 @@ func TestMsgJoin(t *testing.T) {

type UserhostSplitTests struct {
Tests []struct {
Desc string
Source string
Atoms struct {
Nick string
Expand All @@ -235,15 +239,15 @@ func TestUserhostSplit(t *testing.T) {

assert.Equal(t,
test.Atoms.Nick, prefix.Name,
"Name did not match for input: %q", test.Source,
"%s: Name did not match for input: %q", test.Desc, test.Source,
)
assert.Equal(t,
test.Atoms.User, prefix.User,
"User did not match for input: %q", test.Source,
"%s: User did not match for input: %q", test.Desc, test.Source,
)
assert.Equal(t,
test.Atoms.Host, prefix.Host,
"Host did not match for input: %q", test.Source,
"%s: Host did not match for input: %q", test.Desc, test.Source,
)
}
}
2 changes: 1 addition & 1 deletion testcases

0 comments on commit 29f1845

Please sign in to comment.