Merge branch 'master' into feature/webirc

This commit is contained in:
Qais Patankar 2017-12-24 19:08:02 +00:00 committed by GitHub
commit d5dbd683d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 231 additions and 41 deletions

141
irc.go
View File

@ -74,9 +74,9 @@ func (irc *Connection) readLoop() {
irc.Log.Printf("<-- %s\n", strings.TrimSpace(msg)) irc.Log.Printf("<-- %s\n", strings.TrimSpace(msg))
} }
irc.Lock() irc.lastMessageMutex.Lock()
irc.lastMessage = time.Now() irc.lastMessage = time.Now()
irc.Unlock() irc.lastMessageMutex.Unlock()
event, err := parseToEvent(msg) event, err := parseToEvent(msg)
event.Connection = irc event.Connection = irc
if err == nil { if err == nil {
@ -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]
@ -166,9 +197,11 @@ func (irc *Connection) pingLoop() {
select { select {
case <-ticker.C: case <-ticker.C:
//Ping if we haven't received anything from the server within the keep alive period //Ping if we haven't received anything from the server within the keep alive period
irc.lastMessageMutex.Lock()
if time.Since(irc.lastMessage) >= irc.KeepAlive { if time.Since(irc.lastMessage) >= irc.KeepAlive {
irc.SendRawf("PING %d", time.Now().UnixNano()) irc.SendRawf("PING %d", time.Now().UnixNano())
} }
irc.lastMessageMutex.Unlock()
case <-ticker2.C: case <-ticker2.C:
//Ping at the ping frequency //Ping at the ping frequency
irc.SendRawf("PING %d", time.Now().UnixNano()) irc.SendRawf("PING %d", time.Now().UnixNano())
@ -355,6 +388,20 @@ func (irc *Connection) Connected() bool {
// A disconnect sends all buffered messages (if possible), // A disconnect sends all buffered messages (if possible),
// stops all goroutines and then closes the socket. // stops all goroutines and then closes the socket.
func (irc *Connection) Disconnect() { func (irc *Connection) Disconnect() {
irc.Lock()
defer irc.Unlock()
if irc.end != nil {
close(irc.end)
}
irc.end = nil
if irc.pwrite != nil {
close(irc.pwrite)
}
irc.Wait()
if irc.socket != nil { if irc.socket != nil {
irc.socket.Close() irc.socket.Close()
} }
@ -435,26 +482,96 @@ 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
}
realname := irc.user
if irc.RealName != "" {
realname = irc.RealName
}
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, realname)
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.")
} }
} }
// 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")
realname := irc.user
if irc.RealName != "" {
realname = irc.RealName
}
irc.pwrite <- fmt.Sprintf("NICK %s\r\n", irc.nick) 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) irc.pwrite <- fmt.Sprintf("USER %s 0.0.0.0 0.0.0.0 :%s\r\n", irc.user, realname)
return nil return nil
} }

View File

@ -13,13 +13,17 @@ import (
func (irc *Connection) AddCallback(eventcode string, callback func(*Event)) int { func (irc *Connection) AddCallback(eventcode string, callback func(*Event)) int {
eventcode = strings.ToUpper(eventcode) eventcode = strings.ToUpper(eventcode)
id := 0 id := 0
if _, ok := irc.events[eventcode]; !ok {
irc.eventsMutex.Lock()
_, ok := irc.events[eventcode]
if !ok {
irc.events[eventcode] = make(map[int]func(*Event)) irc.events[eventcode] = make(map[int]func(*Event))
id = 0 id = 0
} else { } else {
id = len(irc.events[eventcode]) id = len(irc.events[eventcode])
} }
irc.events[eventcode][id] = callback irc.events[eventcode][id] = callback
irc.eventsMutex.Unlock()
return id return id
} }
@ -28,15 +32,20 @@ func (irc *Connection) AddCallback(eventcode string, callback func(*Event)) int
func (irc *Connection) RemoveCallback(eventcode string, i int) bool { func (irc *Connection) RemoveCallback(eventcode string, i int) bool {
eventcode = strings.ToUpper(eventcode) eventcode = strings.ToUpper(eventcode)
if event, ok := irc.events[eventcode]; ok { irc.eventsMutex.Lock()
event, ok := irc.events[eventcode]
if ok {
if _, ok := event[i]; ok { if _, ok := event[i]; ok {
delete(irc.events[eventcode], i) delete(irc.events[eventcode], i)
irc.eventsMutex.Unlock()
return true return true
} }
irc.Log.Printf("Event found, but no callback found at id %d\n", i) irc.Log.Printf("Event found, but no callback found at id %d\n", i)
irc.eventsMutex.Unlock()
return false return false
} }
irc.eventsMutex.Unlock()
irc.Log.Println("Event not found") irc.Log.Println("Event not found")
return false return false
} }
@ -46,10 +55,14 @@ func (irc *Connection) RemoveCallback(eventcode string, i int) bool {
func (irc *Connection) ClearCallback(eventcode string) bool { func (irc *Connection) ClearCallback(eventcode string) bool {
eventcode = strings.ToUpper(eventcode) eventcode = strings.ToUpper(eventcode)
if _, ok := irc.events[eventcode]; ok { irc.eventsMutex.Lock()
_, ok := irc.events[eventcode]
if ok {
irc.events[eventcode] = make(map[int]func(*Event)) irc.events[eventcode] = make(map[int]func(*Event))
irc.eventsMutex.Unlock()
return true return true
} }
irc.eventsMutex.Unlock()
irc.Log.Println("Event not found") irc.Log.Println("Event not found")
return false return false
@ -59,7 +72,10 @@ func (irc *Connection) ClearCallback(eventcode string) bool {
func (irc *Connection) ReplaceCallback(eventcode string, i int, callback func(*Event)) { func (irc *Connection) ReplaceCallback(eventcode string, i int, callback func(*Event)) {
eventcode = strings.ToUpper(eventcode) eventcode = strings.ToUpper(eventcode)
if event, ok := irc.events[eventcode]; ok { irc.eventsMutex.Lock()
event, ok := irc.events[eventcode]
irc.eventsMutex.Unlock()
if ok {
if _, ok := event[i]; ok { if _, ok := event[i]; ok {
event[i] = callback event[i] = callback
return return
@ -109,7 +125,10 @@ func (irc *Connection) RunCallbacks(event *Event) {
event.Arguments[len(event.Arguments)-1] = msg event.Arguments[len(event.Arguments)-1] = msg
} }
if callbacks, ok := irc.events[event.Code]; ok { irc.eventsMutex.Lock()
callbacks, ok := irc.events[event.Code]
irc.eventsMutex.Unlock()
if ok {
if irc.VerboseCallbackHandler { if irc.VerboseCallbackHandler {
irc.Log.Printf("%v (%v) >> %#v\n", event.Code, len(callbacks), event) irc.Log.Printf("%v (%v) >> %#v\n", event.Code, len(callbacks), event)
} }
@ -121,12 +140,15 @@ func (irc *Connection) RunCallbacks(event *Event) {
irc.Log.Printf("%v (0) >> %#v\n", event.Code, event) irc.Log.Printf("%v (0) >> %#v\n", event.Code, event)
} }
if callbacks, ok := irc.events["*"]; ok { irc.eventsMutex.Lock()
allcallbacks, ok := irc.events["*"]
irc.eventsMutex.Unlock()
if ok {
if irc.VerboseCallbackHandler { if irc.VerboseCallbackHandler {
irc.Log.Printf("%v (0) >> %#v\n", event.Code, event) irc.Log.Printf("%v (0) >> %#v\n", event.Code, event)
} }
for _, callback := range callbacks { for _, callback := range allcallbacks {
callback(event) callback(event)
} }
} }
@ -136,9 +158,6 @@ func (irc *Connection) RunCallbacks(event *Event) {
func (irc *Connection) setupCallbacks() { func (irc *Connection) setupCallbacks() {
irc.events = make(map[string]map[int]func(*Event)) irc.events = make(map[string]map[int]func(*Event))
//Handle error events.
irc.AddCallback("ERROR", func(e *Event) { irc.Disconnect() })
//Handle ping events //Handle ping events
irc.AddCallback("PING", func(e *Event) { irc.SendRaw("PONG :" + e.Message()) }) irc.AddCallback("PING", func(e *Event) { irc.SendRaw("PONG :" + e.Message()) })

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

@ -17,13 +17,15 @@ type Connection struct {
sync.WaitGroup sync.WaitGroup
Debug bool Debug bool
Error chan error Error chan error
WebIRC string
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
WebIRC string
TLSConfig *tls.Config TLSConfig *tls.Config
Version string Version string
Timeout time.Duration Timeout time.Duration
@ -31,6 +33,9 @@ type Connection struct {
KeepAlive time.Duration KeepAlive time.Duration
Server string Server string
RealName string // The real name we want to display.
// If zero-value defaults to the user.
socket net.Conn socket net.Conn
pwrite chan string pwrite chan string
end chan struct{} end chan struct{}
@ -40,9 +45,11 @@ type Connection struct {
user string user string
registered bool registered bool
events map[string]map[int]func(*Event) events map[string]map[int]func(*Event)
eventsMutex sync.Mutex
QuitMessage string QuitMessage string
lastMessage time.Time lastMessage time.Time
lastMessageMutex sync.Mutex
VerboseCallbackHandler bool VerboseCallbackHandler bool
Log *log.Logger Log *log.Logger
@ -60,6 +67,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
} }