client: Handle capabilities, improve handling and testing
This commit is contained in:
parent
6d960c81bc
commit
1cb8e4762b
79
client/capabilities.go
Normal file
79
client/capabilities.go
Normal file
@ -0,0 +1,79 @@
|
||||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
package gircclient
|
||||
|
||||
import "strings"
|
||||
import "sort"
|
||||
|
||||
// ClientCapabilities holds the capabilities that can and have been enabled on
|
||||
// a ServerConnection.
|
||||
type ClientCapabilities struct {
|
||||
Avaliable map[string]*string
|
||||
Enabled map[string]*string
|
||||
Wanted []string
|
||||
}
|
||||
|
||||
// NewClientCapabilities returns a newly-initialised ClientCapabilities.
|
||||
func NewClientCapabilities() ClientCapabilities {
|
||||
var cc ClientCapabilities
|
||||
|
||||
cc.Avaliable = make(map[string]*string, 0)
|
||||
cc.Enabled = make(map[string]*string, 0)
|
||||
cc.Wanted = make([]string, 0)
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
// AddWantedCaps adds the given capabilities to our list of capabilities that
|
||||
// we want from the server.
|
||||
func (cc *ClientCapabilities) AddWantedCaps(caps ...string) {
|
||||
for _, name := range caps {
|
||||
// I'm not sure how fast this is, but speed isn't too much of a concern
|
||||
// here. Adding 'wanted capabilities' is something that generally only
|
||||
// happens at startup anyway.
|
||||
i := sort.Search(len(cc.Wanted), func(i int) bool { return cc.Wanted[i] >= name })
|
||||
|
||||
if i >= len(cc.Wanted) || cc.Wanted[i] != name {
|
||||
cc.Wanted = append(cc.Wanted, name)
|
||||
sort.Strings(cc.Wanted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddCaps adds capabilities from LS lists to our Avaliable map.
|
||||
func (cc *ClientCapabilities) AddCaps(tags []string) {
|
||||
var name string
|
||||
var value *string
|
||||
|
||||
for _, tag := range tags {
|
||||
if strings.Contains(tag, "=") {
|
||||
vals := strings.SplitN(tag, "=", 2)
|
||||
name = vals[0]
|
||||
value = &vals[1]
|
||||
} else {
|
||||
name = tag
|
||||
value = nil
|
||||
}
|
||||
|
||||
cc.Avaliable[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
// ToRequestLine returns a line of capabilities to request, to be used in a
|
||||
// CAP REQ line.
|
||||
func (cc *ClientCapabilities) ToRequestLine() string {
|
||||
var caps []string
|
||||
caps = make([]string, 0)
|
||||
|
||||
for _, name := range cc.Wanted {
|
||||
_, capIsAvailable := cc.Avaliable[name]
|
||||
_, capIsEnabled := cc.Enabled[name]
|
||||
|
||||
if capIsAvailable && !capIsEnabled {
|
||||
caps = append(caps, name)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(caps, " ")
|
||||
}
|
@ -17,8 +17,9 @@ import (
|
||||
|
||||
// ServerConnection is a connection to a single server.
|
||||
type ServerConnection struct {
|
||||
Name string
|
||||
Connected bool
|
||||
Name string
|
||||
Connected bool
|
||||
Registered bool
|
||||
|
||||
// internal stuff
|
||||
connection net.Conn
|
||||
@ -26,8 +27,8 @@ type ServerConnection struct {
|
||||
eventsOut eventmgr.EventManager
|
||||
|
||||
// data we keep track of
|
||||
//features ServerFeatures
|
||||
//caps ClientCapabilities
|
||||
// Features ServerFeatures
|
||||
Caps ClientCapabilities
|
||||
|
||||
// details users must supply before connection
|
||||
Nick string
|
||||
@ -36,6 +37,19 @@ type ServerConnection struct {
|
||||
InitialRealName string
|
||||
}
|
||||
|
||||
// newServerConnection returns an initialised ServerConnection, for internal
|
||||
// use.
|
||||
func newServerConnection() *ServerConnection {
|
||||
var sc ServerConnection
|
||||
|
||||
sc.Caps = NewClientCapabilities()
|
||||
|
||||
sc.Caps.AddWantedCaps("account-notify", "away-notify", "extended-join", "multi-prefix", "sasl")
|
||||
sc.Caps.AddWantedCaps("account-tag", "chghost", "echo-message", "invite-notify", "server-time", "userhost-in-names")
|
||||
|
||||
return &sc
|
||||
}
|
||||
|
||||
// Connect connects to the given address.
|
||||
func (sc *ServerConnection) Connect(address string, ssl bool, tlsconfig *tls.Config) error {
|
||||
var conn net.Conn
|
||||
@ -54,9 +68,7 @@ func (sc *ServerConnection) Connect(address string, ssl bool, tlsconfig *tls.Con
|
||||
sc.connection = conn
|
||||
sc.Connected = true
|
||||
|
||||
sc.Nick = sc.InitialNick
|
||||
sc.Send(nil, "", "NICK", sc.InitialNick)
|
||||
sc.Send(nil, "", "USER", sc.InitialUser, "0", "*", sc.InitialRealName)
|
||||
sc.Send(nil, "", "CAP", "LS", "302")
|
||||
|
||||
go sc.receiveLoop()
|
||||
|
||||
@ -97,7 +109,8 @@ func (sc *ServerConnection) receiveLoop() {
|
||||
info["command"] = message.Command
|
||||
info["params"] = message.Params
|
||||
|
||||
sc.dispatchIn(message.Command, info)
|
||||
// IRC commands are case-insensitive
|
||||
sc.dispatchIn(strings.ToUpper(message.Command), info)
|
||||
}
|
||||
|
||||
sc.connection.Close()
|
||||
|
44
client/handlers.go
Normal file
44
client/handlers.go
Normal file
@ -0,0 +1,44 @@
|
||||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
package gircclient
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/DanielOaks/girc-go/eventmgr"
|
||||
)
|
||||
|
||||
// welcomeHandler sets the nick to the first parameter of the 001 message.
|
||||
// This ensures that when we connect to IRCds that silently truncate the
|
||||
// nickname, we keep the correct one.
|
||||
func welcomeHandler(event string, info eventmgr.InfoMap) {
|
||||
sc := info["server"].(*ServerConnection)
|
||||
sc.Nick = info["params"].([]string)[0]
|
||||
}
|
||||
|
||||
func capHandler(event string, info eventmgr.InfoMap) {
|
||||
sc := info["server"].(*ServerConnection)
|
||||
params := info["params"].([]string)
|
||||
subcommand := strings.ToUpper(params[1])
|
||||
if !sc.Registered && (subcommand == "ACK" || subcommand == "NAK") {
|
||||
sendRegistration(sc)
|
||||
} else if subcommand == "LS" {
|
||||
if len(params) > 3 {
|
||||
sc.Caps.AddCaps(strings.Split(params[3], " "))
|
||||
} else {
|
||||
sc.Caps.AddCaps(strings.Split(params[2], " "))
|
||||
capsToRequest := sc.Caps.ToRequestLine()
|
||||
|
||||
if len(capsToRequest) > 0 {
|
||||
sc.Send(nil, "", "CAP", "REQ", capsToRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendRegistration(sc *ServerConnection) {
|
||||
sc.Nick = sc.InitialNick
|
||||
sc.Send(nil, "", "NICK", sc.InitialNick)
|
||||
sc.Send(nil, "", "USER", sc.InitialUser, "0", "*", sc.InitialRealName)
|
||||
}
|
@ -20,14 +20,6 @@ type Reactor struct {
|
||||
eventsToRegister []eventRegistration
|
||||
}
|
||||
|
||||
// welcomeHandler sets the nick to the first parameter of the 001 message.
|
||||
// This ensures that when we connect to IRCds that silently truncate the
|
||||
// nickname, we keep the correct one.
|
||||
func welcomeHandler(event string, info eventmgr.InfoMap) {
|
||||
server := info["server"].(*ServerConnection)
|
||||
server.Nick = info["params"].([]string)[0]
|
||||
}
|
||||
|
||||
// NewReactor returns a new, empty Reactor.
|
||||
func NewReactor() Reactor {
|
||||
var newReactor Reactor
|
||||
@ -36,6 +28,7 @@ func NewReactor() Reactor {
|
||||
newReactor.eventsToRegister = make([]eventRegistration, 0)
|
||||
|
||||
// add the default handlers
|
||||
newReactor.RegisterEvent("in", "CAP", capHandler, -10)
|
||||
newReactor.RegisterEvent("in", "001", welcomeHandler, -10)
|
||||
|
||||
return newReactor
|
||||
@ -43,15 +36,15 @@ func NewReactor() Reactor {
|
||||
|
||||
// CreateServer creates a ServerConnection and returns it.
|
||||
func (r *Reactor) CreateServer(name string) *ServerConnection {
|
||||
var sc ServerConnection
|
||||
sc := newServerConnection()
|
||||
|
||||
r.ServerConnections[name] = &sc
|
||||
r.ServerConnections[name] = sc
|
||||
|
||||
for _, e := range r.eventsToRegister {
|
||||
sc.RegisterEvent(e.Direction, e.Name, e.Handler, e.Priority)
|
||||
}
|
||||
|
||||
return &sc
|
||||
return sc
|
||||
}
|
||||
|
||||
// Shutdown shuts down all ServerConnections.
|
||||
|
@ -15,6 +15,8 @@ import (
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DanielOaks/girc-go/ircmsg"
|
||||
)
|
||||
|
||||
func TestPlainConnection(t *testing.T) {
|
||||
@ -95,13 +97,56 @@ func TestTLSConnection(t *testing.T) {
|
||||
testServerConnection(t, reactor, client, listener)
|
||||
}
|
||||
|
||||
func sendMessage(conn net.Conn, tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) {
|
||||
ircmsg := ircmsg.MakeMessage(tags, prefix, command, params...)
|
||||
line, err := ircmsg.Line()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(conn, line)
|
||||
|
||||
// need to wait for a quick moment here for TLS to do this properly
|
||||
runtime.Gosched()
|
||||
waitTime, _ := time.ParseDuration("10ms")
|
||||
time.Sleep(waitTime)
|
||||
}
|
||||
|
||||
func testServerConnection(t *testing.T, reactor Reactor, client *ServerConnection, listener net.Listener) {
|
||||
conn, _ := listener.Accept()
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
// test each message in sequence
|
||||
var message string
|
||||
|
||||
// CAP
|
||||
message, _ = reader.ReadString('\n')
|
||||
if message != "CAP LS 302\r\n" {
|
||||
t.Error(
|
||||
"Did not receive CAP LS message, received: [",
|
||||
message,
|
||||
"]",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
sendMessage(conn, nil, "example.com", "CAP", "*", "LS", "*", "multi-prefix userhost-in-names sasl=PLAIN")
|
||||
sendMessage(conn, nil, "example.com", "CAP", "*", "LS", "chghost")
|
||||
|
||||
message, _ = reader.ReadString('\n')
|
||||
if message != "CAP REQ :chghost multi-prefix sasl userhost-in-names\r\n" {
|
||||
t.Error(
|
||||
"Did not receive CAP REQ message, received: [",
|
||||
message,
|
||||
"]",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// these should be silently ignored
|
||||
fmt.Fprintf(conn, "\r\n\r\n\r\n")
|
||||
|
||||
sendMessage(conn, nil, "example.com", "CAP", "*", "ACK", "chghost multi-prefix userhost-in-names sasl")
|
||||
|
||||
// NICK/USER
|
||||
message, _ = reader.ReadString('\n')
|
||||
if message != "NICK coolguy\r\n" {
|
||||
t.Error(
|
||||
@ -123,12 +168,7 @@ func testServerConnection(t *testing.T, reactor Reactor, client *ServerConnectio
|
||||
}
|
||||
|
||||
// make sure nick changes properly
|
||||
// need to wait for a quick moment here for TLS to do this properly
|
||||
fmt.Fprintf(conn, "\r\n\r\n\r\n") // these should be silently ignored
|
||||
fmt.Fprintf(conn, ":example.com 001 dan :Welcome to the gIRC-Go Test Network!\r\n")
|
||||
runtime.Gosched()
|
||||
waitTime, _ := time.ParseDuration("10ms")
|
||||
time.Sleep(waitTime)
|
||||
sendMessage(conn, nil, "example.com", "001", "dan", "Welcome to the gIRC-Go Test Network!")
|
||||
|
||||
if client.Nick != "dan" {
|
||||
t.Error(
|
||||
|
Loading…
Reference in New Issue
Block a user