Merge pull request #94 from irccloud/ircv3-tags

IRCv3 capability negotiation and tag parsing
This commit is contained in:
Thomas Jager 2017-10-13 19:30:29 +02:00 committed by GitHub
commit ef65ae61a3
5 changed files with 165 additions and 27 deletions

111
irc.go
View File

@ -87,6 +87,17 @@ func (irc *Connection) readLoop() {
} }
} }
// Unescape tag values as defined in the IRCv3.2 message tags spec
// http://ircv3.net/specs/core/message-tags-3.2.html
func unescapeTagValue(value string) string {
value = strings.Replace(value, "\\:", ";", -1)
value = strings.Replace(value, "\\s", " ", -1)
value = strings.Replace(value, "\\\\", "\\", -1)
value = strings.Replace(value, "\\r", "\r", -1)
value = strings.Replace(value, "\\n", "\n", -1)
return value
}
//Parse raw irc messages //Parse raw irc messages
func parseToEvent(msg string) (*Event, error) { func parseToEvent(msg string) (*Event, error) {
msg = strings.TrimSuffix(msg, "\n") //Remove \r\n msg = strings.TrimSuffix(msg, "\n") //Remove \r\n
@ -95,6 +106,26 @@ func parseToEvent(msg string) (*Event, error) {
if len(msg) < 5 { if len(msg) < 5 {
return nil, errors.New("Malformed msg from server") return nil, errors.New("Malformed msg from server")
} }
if msg[0] == '@' {
// IRCv3 Message Tags
if i := strings.Index(msg, " "); i > -1 {
event.Tags = make(map[string]string)
tags := strings.Split(msg[1:i], ";")
for _, data := range tags {
parts := strings.SplitN(data, "=", 2)
if len(parts) == 1 {
event.Tags[parts[0]] = ""
} else {
event.Tags[parts[0]] = unescapeTagValue(parts[1])
}
}
msg = msg[i+1 : len(msg)]
} else {
return nil, errors.New("Malformed msg from server")
}
}
if msg[0] == ':' { if msg[0] == ':' {
if i := strings.Index(msg, " "); i > -1 { if i := strings.Index(msg, " "); i > -1 {
event.Source = msg[1:i] event.Source = msg[1:i]
@ -432,26 +463,84 @@ func (irc *Connection) Connect(server string) error {
irc.pwrite <- fmt.Sprintf("PASS %s\r\n", irc.Password) irc.pwrite <- fmt.Sprintf("PASS %s\r\n", irc.Password)
} }
resChan := make(chan *SASLResult) err = irc.negotiateCaps()
if err != nil {
return err
}
irc.pwrite <- fmt.Sprintf("NICK %s\r\n", irc.nick)
irc.pwrite <- fmt.Sprintf("USER %s 0.0.0.0 0.0.0.0 :%s\r\n", irc.user, irc.user)
return nil
}
// Negotiate IRCv3 capabilities
func (irc *Connection) negotiateCaps() error {
saslResChan := make(chan *SASLResult)
if irc.UseSASL {
irc.RequestCaps = append(irc.RequestCaps, "sasl")
irc.setupSASLCallbacks(saslResChan)
}
if len(irc.RequestCaps) == 0 {
return nil
}
cap_chan := make(chan bool, len(irc.RequestCaps))
irc.AddCallback("CAP", func(e *Event) {
if len(e.Arguments) != 3 {
return
}
command := e.Arguments[1]
if command == "LS" {
missing_caps := len(irc.RequestCaps)
for _, cap_name := range strings.Split(e.Arguments[2], " ") {
for _, req_cap := range irc.RequestCaps {
if cap_name == req_cap {
irc.pwrite <- fmt.Sprintf("CAP REQ :%s\r\n", cap_name)
missing_caps--
}
}
}
for i := 0; i < missing_caps; i++ {
cap_chan <- true
}
} else if command == "ACK" || command == "NAK" {
for _, cap_name := range strings.Split(strings.TrimSpace(e.Arguments[2]), " ") {
if cap_name == "" {
continue
}
if command == "ACK" {
irc.AcknowledgedCaps = append(irc.AcknowledgedCaps, cap_name)
}
cap_chan <- true
}
}
})
irc.pwrite <- "CAP LS\r\n"
if irc.UseSASL { if irc.UseSASL {
irc.setupSASLCallbacks(resChan)
irc.pwrite <- fmt.Sprintf("CAP LS\r\n")
// request SASL
irc.pwrite <- fmt.Sprintf("CAP REQ :sasl\r\n")
// if sasl request doesn't complete in 15 seconds, close chan and timeout
select { select {
case res := <-resChan: case res := <-saslResChan:
if res.Failed { if res.Failed {
close(resChan) close(saslResChan)
return res.Err return res.Err
} }
case <-time.After(time.Second * 15): case <-time.After(time.Second * 15):
close(resChan) close(saslResChan)
return errors.New("SASL setup timed out. This shouldn't happen.") return errors.New("SASL setup timed out. This shouldn't happen.")
} }
} }
irc.pwrite <- fmt.Sprintf("NICK %s\r\n", irc.nick)
irc.pwrite <- fmt.Sprintf("USER %s 0.0.0.0 0.0.0.0 :%s\r\n", irc.user, irc.user) // Wait for all capabilities to be ACKed or NAKed before ending negotiation
for i := 0; i < len(irc.RequestCaps); i++ {
<-cap_chan
}
irc.pwrite <- fmt.Sprintf("CAP END\r\n")
return nil return nil
} }

47
irc_parse_test.go Normal file
View File

@ -0,0 +1,47 @@
package irc
import (
"fmt"
"testing"
)
func checkResult(t *testing.T, event *Event) {
if event.Nick != "nick" {
t.Fatal("Parse failed: nick")
}
if event.User != "~user" {
t.Fatal("Parse failed: user")
}
if event.Code != "PRIVMSG" {
t.Fatal("Parse failed: code")
}
if event.Arguments[0] != "#channel" {
t.Fatal("Parse failed: channel")
}
if event.Arguments[1] != "message text" {
t.Fatal("Parse failed: message")
}
}
func TestParse(t *testing.T) {
event, err := parseToEvent(":nick!~user@host PRIVMSG #channel :message text")
if err != nil {
t.Fatal("Parse PRIVMSG failed")
}
checkResult(t, event)
}
func TestParseTags(t *testing.T) {
event, err := parseToEvent("@tag;+tag2=raw+:=,escaped\\:\\s\\\\ :nick!~user@host PRIVMSG #channel :message text")
if err != nil {
t.Fatal("Parse PRIVMSG with tags failed")
}
checkResult(t, event)
fmt.Printf("%s", event.Tags)
if _, ok := event.Tags["tag"]; !ok {
t.Fatal("Parsing value-less tag failed")
}
if event.Tags["+tag2"] != "raw+:=,escaped; \\" {
t.Fatal("Parsing tag failed")
}
}

View File

@ -43,7 +43,6 @@ func (irc *Connection) setupSASLCallbacks(result chan<- *SASLResult) {
result <- &SASLResult{true, errors.New(e.Arguments[1])} result <- &SASLResult{true, errors.New(e.Arguments[1])}
}) })
irc.AddCallback("903", func(e *Event) { irc.AddCallback("903", func(e *Event) {
irc.SendRaw("CAP END")
result <- &SASLResult{false, nil} result <- &SASLResult{false, nil}
}) })
irc.AddCallback("904", func(e *Event) { irc.AddCallback("904", func(e *Event) {

View File

@ -34,7 +34,7 @@ func TestConnectionSASL(t *testing.T) {
err := irccon.Connect(SASLServer) err := irccon.Connect(SASLServer)
if err != nil { if err != nil {
t.Fatal("SASL failed") t.Fatalf("SASL failed: %s", err)
} }
irccon.Loop() irccon.Loop()
} }

View File

@ -20,6 +20,8 @@ type Connection struct {
Password string Password string
UseTLS bool UseTLS bool
UseSASL bool UseSASL bool
RequestCaps []string
AcknowledgedCaps []string
SASLLogin string SASLLogin string
SASLPassword string SASLPassword string
SASLMech string SASLMech string
@ -61,6 +63,7 @@ type Event struct {
Source string //<host> Source string //<host>
User string //<usr> User string //<usr>
Arguments []string Arguments []string
Tags map[string]string
Connection *Connection Connection *Connection
} }