initial fixes and refactoring

This commit is contained in:
Shivaram Lingamneni 2021-02-19 03:55:06 -05:00
parent 9d5dccb821
commit dc36bd80a6
15 changed files with 1273 additions and 1010 deletions

@ -1,10 +1,17 @@
.PHONY: test ircevent gofmt
test:
cd ircfmt && go test . && go vet .
cd ircmap && go test . && go vet .
cd ircmsg && go test . && go vet .
cd ircreader && go test . && go vet .
cd ircutils && go test . && go vet .
$(info Note: ircevent must be tested separately)
./.check-gofmt.sh
# ircevent requires a local ircd for testing, plus some env vars
ircevent:
cd ircevent && go test . && go vet .
gofmt:
./.check-gofmt.sh --fix

@ -19,11 +19,13 @@ An example bot that uses these packages can be found [here](https://gist.github.
Packages:
* [**gircclient**](https://godoc.org/github.com/goshuirc/irc-go/client): Very work-in-progress client library.
* [**ircmsg**](https://godoc.org/github.com/goshuirc/irc-go/ircmsg): IRC message handling, raw line parsing and creation.
* [**ircfmt**](https://godoc.org/github.com/goshuirc/irc-go/ircfmt): IRC format codes handling, escaping and unescaping.
* [**ircreader**](https://godoc.org/github.com/goshuirc/irc-go/ircreader): Optimized reader for \n-terminated lines, with an expanding but bounded buffer.
* [**ircevent**](https://godoc.org/github.com/goshuirc/irc-go/ircevent): Work-in-progress client library (fork of [thoj/go-ircevent](https://github.com/thoj/go-ircevent)).
* [**gircclient**](https://godoc.org/github.com/goshuirc/irc-go/client): Another work-in-progress client library.
* [**ircmap**](https://godoc.org/github.com/goshuirc/irc-go/ircmap): IRC string casefolding.
* [**ircmatch**](https://godoc.org/github.com/goshuirc/irc-go/ircmatch): IRC string matching (mostly just a globbing engine).
* [**ircmsg**](https://godoc.org/github.com/goshuirc/irc-go/ircmsg): IRC message handling, raw line parsing and creation.
* [**ircutils**](https://godoc.org/github.com/goshuirc/irc-go/ircutils): Useful utility functions and classes that don't fit into their own packages.
---

@ -1,4 +1,5 @@
// Copyright (c) 2009 Thomas Jager. All rights reserved.
// Copyright (c) 2021 Shivaram Lingamneni.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are

@ -1,79 +1,54 @@
Description
-----------
Event based irc client library.
This is an event-based IRC client library. It is a fork of [thoj/go-ircevent](https://github.com/thoj-ircevent).
Features
--------
* Event based. Register Callbacks for the events you need to handle.
* Handles basic irc demands for you
* Standard CTCP
* Reconnections on errors
* Detect stoned servers
Install
-------
$ go get github.com/thoj/go-ircevent
* Event-based: register callbacks for IRC commands
* Handles reconnections
* Supports SASL
* Supports requesting [IRCv3 capabilities](https://ircv3.net/specs/core/capability-negotiation)
Example
-------
See [examples/simple/simple.go](examples/simple/simple.go) and [irc_test.go](irc_test.go)
See [examples/simple.go](examples/simple.go) for a working example, but this illustrates the API:
Events for callbacks
--------------------
* 001 Welcome
* PING
* CTCP Unknown CTCP
* CTCP_VERSION Version request (Handled internaly)
* CTCP_USERINFO
* CTCP_CLIENTINFO
* CTCP_TIME
* CTCP_PING
* CTCP_ACTION (/me)
* PRIVMSG
* MODE
* JOIN
```go
irc := ircevent.Connection{
Server: "testnet.oragono.io:6697",
UseTLS: true,
Nick: "ircevent-test",
Debug: true,
RequestCaps: []string{"server-time", "message-tags"},
}
+Many more
irc.AddCallback("001", func(e ircevent.Event) { irc.Join("#ircevent-test") })
irc.AddCallback("PRIVMSG", func(event ircevent.Event) {
//event.Message() contains the message
//event.Nick() Contains the sender
//event.Params[0] Contains the channel
});
AddCallback Example
-------------------
ircobj.AddCallback("PRIVMSG", func(event *irc.Event) {
//event.Message() contains the message
//event.Nick Contains the sender
//event.Arguments[0] Contains the channel
});
Please note: Callbacks are run in the main thread. If a callback needs a long
time to execute please run it in a new thread.
Example:
ircobj.AddCallback("PRIVMSG", func(event *irc.Event) {
go func(event *irc.Event) {
//event.Message() contains the message
//event.Nick Contains the sender
//event.Arguments[0] Contains the channel
}(event)
});
err := irc.Connect()
if err != nil {
log.Fatal(err)
}
irc.Loop()
```
The read loop will wait for all callbacks to complete before moving on
to the next message. If your callback needs to trigger a long-running task,
you should spin off a new goroutine for it.
Commands
--------
ircobj := irc.IRC("<nick>", "<user>") //Create new ircobj
//Set options
ircobj.UseTLS = true //default is false
//ircobj.TLSOptions //set ssl options
ircobj.Password = "[server password]"
//Commands
ircobj.Connect("irc.someserver.com:6667") //Connect to server
ircobj.SendRaw("<string>") //sends string to server. Adds \r\n
ircobj.SendRawf("<formatstring>", ...) //sends formatted string to server.n
ircobj.Join("<#channel> [password]")
ircobj.Nick("newnick")
ircobj.Privmsg("<nickname | #channel>", "msg") // sends a message to either a certain nick or a channel
ircobj.Privmsgf(<nickname | #channel>, "<formatstring>", ...)
ircobj.Notice("<nickname | #channel>", "msg")
ircobj.Noticef("<nickname | #channel>", "<formatstring>", ...)
These commands can be used from inside callbacks, or externally:
irc.Connect("irc.someserver.com:6667") //Connect to server
irc.Send(command, params...)
irc.SendWithTags(tags, command, params...)
irc.Join(channel)
irc.Privmsg(target, message)
irc.Privmsgf(target, formatString, params...)

@ -1,27 +1,69 @@
package main
import (
"github.com/thoj/go-ircevent"
"crypto/tls"
"fmt"
"os"
"strconv"
"strings"
"github.com/goshuirc/irc-go/ircevent"
)
const channel = "#go-eventirc-test";
const serverssl = "irc.freenode.net:7000"
func getenv(key, defaultValue string) (value string) {
value = os.Getenv(key)
if value == "" {
value = defaultValue
}
return
}
func main() {
ircnick1 := "blatiblat"
irccon := irc.IRC(ircnick1, "IRCTestSSL")
irccon.VerboseCallbackHandler = true
irccon.Debug = true
irccon.UseTLS = true
irccon.TLSConfig = &tls.Config{InsecureSkipVerify: true}
irccon.AddCallback("001", func(e *irc.Event) { irccon.Join(channel) })
irccon.AddCallback("366", func(e *irc.Event) { })
err := irccon.Connect(serverssl)
nick := getenv("IRCEVENT_NICK", "robot")
server := getenv("IRCEVENT_SERVER", "localhost:6697")
channel := getenv("IRCEVENT_CHANNEL", "#ircevent-test")
irc := &ircevent.Connection{
Server: server,
Nick: nick,
Debug: true,
UseTLS: true,
TLSConfig: &tls.Config{InsecureSkipVerify: true},
RequestCaps: []string{"server-time", "message-tags"},
}
irc.AddCallback("001", func(e ircevent.Event) { irc.Join(channel) })
irc.AddCallback("JOIN", func(e ircevent.Event) {}) // TODO try to rejoin if we *don't* get this
irc.AddCallback("PRIVMSG", func(e ircevent.Event) {
if len(e.Params) < 2 {
return
}
text := e.Params[1]
if strings.HasPrefix(text, nick) {
irc.Privmsg(e.Params[0], "don't @ me, fleshbag")
} else if text == "xyzzy" {
// this causes the server to disconnect us and the program to exit
irc.Quit()
} else if text == "plugh" {
// this causes the server to disconnect us, but the client will reconnect
irc.Send("QUIT", "I'LL BE BACK")
} else if text == "wwssadadba" {
// this line intentionally panics; the client will recover from it
irc.Privmsg(e.Params[0], e.Params[2])
}
})
// example client-to-client extension via message-tags:
// have the bot maintain a running sum of integers
var sum int64 // doesn't need synchronization as long as it's only visible from a single callback
irc.AddCallback("TAGMSG", func(e ircevent.Event) {
_, tv := e.GetTag("+summand")
if v, err := strconv.ParseInt(tv, 10, 64); err == nil {
sum += v
irc.SendWithTags(map[string]string{"+sum": strconv.FormatInt(sum, 10)}, "TAGMSG", e.Params[0])
}
})
err := irc.Connect()
if err != nil {
fmt.Printf("Err %s", err )
return
}
irccon.Loop()
irc.Loop()
}

@ -0,0 +1,68 @@
package main
import (
"log"
"os"
"strconv"
"net/http"
_ "net/http/pprof"
"github.com/goshuirc/irc-go/ircevent"
)
/*
Flooding stress test (responds to its own echo messages in a loop);
don't run this against a real IRC server!
*/
func getenv(key, defaultValue string) (value string) {
value = os.Getenv(key)
if value == "" {
value = defaultValue
}
return
}
func main() {
ps := http.Server{
Addr: getenv("IRCEVENT_PPROF_LISTENER", "localhost:6077"),
}
go func() {
if err := ps.ListenAndServe(); err != nil {
log.Fatal(err)
}
}()
nick := getenv("IRCEVENT_NICK", "chatterbox")
server := getenv("IRCEVENT_SERVER", "localhost:6667")
channel := getenv("IRCEVENT_CHANNEL", "#ircevent-test")
limit := 0
if envLimit, err := strconv.Atoi(os.Getenv("IRCEVENT_LIMIT")); err == nil {
limit = envLimit
}
irc := &ircevent.Connection{
Server: server,
Nick: nick,
RequestCaps: []string{"server-time", "echo-message"},
}
irc.AddCallback("001", func(e ircevent.Event) { irc.Join(channel) })
irc.AddCallback("JOIN", func(e ircevent.Event) { irc.Privmsg(channel, "hi there friend!") })
// echo whatever we get back
count := 0
irc.AddCallback("PRIVMSG", func(e ircevent.Event) {
if limit != 0 && count >= limit {
irc.Quit()
} else {
irc.Privmsg(e.Params[0], e.Params[1])
count++
}
})
err := irc.Connect()
if err != nil {
return
}
irc.Loop()
}

File diff suppressed because it is too large Load Diff

@ -1,283 +1,274 @@
package irc
package ircevent
import (
"context"
"reflect"
"runtime"
"strconv"
"fmt"
"log"
"runtime/debug"
"strings"
"time"
"github.com/goshuirc/irc-go/ircmsg"
)
// Tuple type for uniquely identifying callbacks
type CallbackID struct {
eventCode string
id uint64
}
// Register a callback to a connection and event code. A callback is a function
// which takes only an Event pointer as parameter. Valid event codes are all
// IRC/CTCP commands and error/response codes. To register a callback for all
// events pass "*" as the event code. This function returns the ID of the
// registered callback for later management.
func (irc *Connection) AddCallback(eventcode string, callback func(*Event)) int {
eventcode = strings.ToUpper(eventcode)
id := 0
func (irc *Connection) AddCallback(eventCode string, callback func(Event)) CallbackID {
eventCode = strings.ToUpper(eventCode)
irc.eventsMutex.Lock()
_, ok := irc.events[eventcode]
if !ok {
irc.events[eventcode] = make(map[int]func(*Event))
id = 0
} else {
id = len(irc.events[eventcode])
defer irc.eventsMutex.Unlock()
if irc.events == nil {
irc.events = make(map[string]map[uint64]Callback)
}
irc.events[eventcode][id] = callback
irc.eventsMutex.Unlock()
_, ok := irc.events[eventCode]
if !ok {
irc.events[eventCode] = make(map[uint64]Callback)
}
id := CallbackID{eventCode: eventCode, id: irc.idCounter}
irc.idCounter++
irc.events[eventCode][id.id] = Callback(callback)
return id
}
// Remove callback i (ID) from the given event code. This functions returns
// true upon success, false if any error occurs.
func (irc *Connection) RemoveCallback(eventcode string, i int) bool {
eventcode = strings.ToUpper(eventcode)
// Remove callback i (ID) from the given event code.
func (irc *Connection) RemoveCallback(id CallbackID) {
irc.eventsMutex.Lock()
event, ok := irc.events[eventcode]
if ok {
if _, ok := event[i]; ok {
delete(irc.events[eventcode], i)
irc.eventsMutex.Unlock()
return true
}
irc.Log.Printf("Event found, but no callback found at id %d\n", i)
irc.eventsMutex.Unlock()
return false
}
irc.eventsMutex.Unlock()
irc.Log.Println("Event not found")
return false
defer irc.eventsMutex.Unlock()
delete(irc.events[id.eventCode], id.id)
}
// Remove all callbacks from a given event code. It returns true
// if given event code is found and cleared.
func (irc *Connection) ClearCallback(eventcode string) bool {
// Remove all callbacks from a given event code.
func (irc *Connection) ClearCallback(eventcode string) {
eventcode = strings.ToUpper(eventcode)
irc.eventsMutex.Lock()
_, ok := irc.events[eventcode]
if ok {
irc.events[eventcode] = make(map[int]func(*Event))
irc.eventsMutex.Unlock()
return true
}
irc.eventsMutex.Unlock()
irc.Log.Println("Event not found")
return false
defer irc.eventsMutex.Unlock()
delete(irc.events, eventcode)
}
// Replace callback i (ID) associated with a given event code with a new callback function.
func (irc *Connection) ReplaceCallback(eventcode string, i int, callback func(*Event)) {
eventcode = strings.ToUpper(eventcode)
func (irc *Connection) ReplaceCallback(id CallbackID, callback func(Event)) bool {
irc.eventsMutex.Lock()
defer irc.eventsMutex.Unlock()
if _, ok := irc.events[id.eventCode][id.id]; ok {
irc.events[id.eventCode][id.id] = callback
return true
}
return false
}
func (irc *Connection) getCallbacks(code string) (result []Callback) {
code = strings.ToUpper(code)
irc.eventsMutex.Lock()
event, ok := irc.events[eventcode]
irc.eventsMutex.Unlock()
if ok {
if _, ok := event[i]; ok {
event[i] = callback
return
}
irc.Log.Printf("Event found, but no callback found at id %d\n", i)
defer irc.eventsMutex.Unlock()
cMap := irc.events[code]
starMap := irc.events["*"]
length := len(cMap) + len(starMap)
if length == 0 {
return
}
irc.Log.Printf("Event not found. Use AddCallBack\n")
result = make([]Callback, 0, length)
for _, c := range cMap {
result = append(result, c)
}
for _, c := range starMap {
result = append(result, c)
}
return
}
// Execute all callbacks associated with a given event.
func (irc *Connection) RunCallbacks(event *Event) {
msg := event.Message()
if event.Code == "PRIVMSG" && len(msg) > 2 && msg[0] == '\x01' {
event.Code = "CTCP" //Unknown CTCP
func (irc *Connection) runCallbacks(msg ircmsg.IRCMessage) {
event := Event{IRCMessage: msg}
if i := strings.LastIndex(msg, "\x01"); i > 0 {
msg = msg[1:i]
} else {
irc.Log.Printf("Invalid CTCP Message: %s\n", strconv.Quote(msg))
return
if irc.EnableCTCP {
eventRewriteCTCP(&event)
}
callbacks := irc.getCallbacks(event.Command)
if len(callbacks) == 0 {
return
}
if irc.CallbackTimeout == 0 {
// just run the callbacks in serial in this case;
// it's not safe for them to take a long time anyway
if !irc.AllowPanic {
defer func() {
if r := recover(); r != nil {
log.Printf("Caught panic in callback: %v\n%s", r, debug.Stack())
}
}()
}
if msg == "VERSION" {
event.Code = "CTCP_VERSION"
for _, callback := range callbacks {
callback(event)
}
} else {
event.Ctx = context.Background()
ctx, cancel := context.WithTimeout(event.Ctx, irc.CallbackTimeout)
event.Ctx = ctx
defer cancel()
} else if msg == "TIME" {
event.Code = "CTCP_TIME"
remaining := len(callbacks)
done := make(chan empty, remaining)
} else if strings.HasPrefix(msg, "PING") {
event.Code = "CTCP_PING"
} else if msg == "USERINFO" {
event.Code = "CTCP_USERINFO"
} else if msg == "CLIENTINFO" {
event.Code = "CTCP_CLIENTINFO"
} else if strings.HasPrefix(msg, "ACTION") {
event.Code = "CTCP_ACTION"
if len(msg) > 6 {
msg = msg[7:]
} else {
msg = ""
}
for _, callback := range callbacks {
go runCallbackConcurrent(event, callback, done, irc.Log, irc.AllowPanic)
}
event.Arguments[len(event.Arguments)-1] = msg
}
irc.eventsMutex.Lock()
callbacks := make(map[int]func(*Event))
eventCallbacks, ok := irc.events[event.Code]
id := 0
if ok {
for _, callback := range eventCallbacks {
callbacks[id] = callback
id++
}
}
allCallbacks, ok := irc.events["*"]
if ok {
for _, callback := range allCallbacks {
callbacks[id] = callback
id++
}
}
irc.eventsMutex.Unlock()
if irc.VerboseCallbackHandler {
irc.Log.Printf("%v (%v) >> %#v\n", event.Code, len(callbacks), event)
}
event.Ctx = context.Background()
if irc.CallbackTimeout != 0 {
event.Ctx, _ = context.WithTimeout(event.Ctx, irc.CallbackTimeout)
}
done := make(chan int)
for id, callback := range callbacks {
go func(id int, done chan<- int, cb func(*Event), event *Event) {
start := time.Now()
cb(event)
for remaining > 0 {
select {
case done <- id:
case <-event.Ctx.Done(): // If we timed out, report how long until we eventually finished
irc.Log.Printf("Canceled callback %s finished in %s >> %#v\n",
getFunctionName(cb),
time.Since(start),
event,
)
case <-done:
remaining--
case <-event.Ctx.Done():
irc.Log.Printf("Timeout on %s while waiting for %d callback(s)\n", event.Command, remaining)
return
}
}(id, done, callback, event)
}
for len(callbacks) > 0 {
select {
case jobID := <-done:
delete(callbacks, jobID)
case <-event.Ctx.Done(): // context timed out!
timedOutCallbacks := []string{}
for _, cb := range callbacks { // Everything left here did not finish
timedOutCallbacks = append(timedOutCallbacks, getFunctionName(cb))
}
irc.Log.Printf("Timeout while waiting for %d callback(s) to finish (%s)\n",
len(callbacks),
strings.Join(timedOutCallbacks, ", "),
)
return
}
}
}
func getFunctionName(f func(*Event)) string {
return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
func runCallbackConcurrent(event Event, callback Callback, done chan empty, log *log.Logger, allowPanic bool) {
if !allowPanic {
defer func() {
if r := recover(); r != nil {
if !allowPanic {
log.Printf("Caught panic in callback: %v\n%s", r, debug.Stack())
}
}
}()
}
callback(event)
done <- empty{}
}
// Set up some initial callbacks to handle the IRC/CTCP protocol.
func (irc *Connection) setupCallbacks() {
irc.events = make(map[string]map[int]func(*Event))
irc.stateMutex.Lock()
needBaseCallbacks := !irc.hasBaseCallbacks
irc.hasBaseCallbacks = true
irc.stateMutex.Unlock()
//Handle ping events
irc.AddCallback("PING", func(e *Event) { irc.SendRaw("PONG :" + e.Message()) })
if !needBaseCallbacks {
return
}
//Version handler
irc.AddCallback("CTCP_VERSION", func(e *Event) {
irc.SendRawf("NOTICE %s :\x01VERSION %s\x01", e.Nick, irc.Version)
})
// PING: we must respond with the correct PONG
irc.AddCallback("PING", func(e Event) { irc.Send("PONG", e.Message()) })
irc.AddCallback("CTCP_USERINFO", func(e *Event) {
irc.SendRawf("NOTICE %s :\x01USERINFO %s\x01", e.Nick, irc.user)
})
irc.AddCallback("CTCP_CLIENTINFO", func(e *Event) {
irc.SendRawf("NOTICE %s :\x01CLIENTINFO PING VERSION TIME USERINFO CLIENTINFO\x01", e.Nick)
})
irc.AddCallback("CTCP_TIME", func(e *Event) {
ltime := time.Now()
irc.SendRawf("NOTICE %s :\x01TIME %s\x01", e.Nick, ltime.String())
})
irc.AddCallback("CTCP_PING", func(e *Event) { irc.SendRawf("NOTICE %s :\x01%s\x01", e.Nick, e.Message()) })
// 437: ERR_UNAVAILRESOURCE "<nick/channel> :Nick/channel is temporarily unavailable"
// Add a _ to current nick. If irc.nickcurrent is empty this cannot
// work. It has to be set somewhere first in case the nick is already
// taken or unavailable from the beginning.
irc.AddCallback("437", func(e *Event) {
// If irc.nickcurrent hasn't been set yet, set to irc.nick
if irc.nickcurrent == "" {
irc.nickcurrent = irc.nick
}
if len(irc.nickcurrent) > 8 {
irc.nickcurrent = "_" + irc.nickcurrent
} else {
irc.nickcurrent = irc.nickcurrent + "_"
}
irc.SendRawf("NICK %s", irc.nickcurrent)
// PONG: record time to make sure the server is responding to us
irc.AddCallback("PONG", func(e Event) {
irc.recordPong(e.Message())
})
// 433: ERR_NICKNAMEINUSE "<nick> :Nickname is already in use"
// Add a _ to current nick.
irc.AddCallback("433", func(e *Event) {
// If irc.nickcurrent hasn't been set yet, set to irc.nick
if irc.nickcurrent == "" {
irc.nickcurrent = irc.nick
}
if len(irc.nickcurrent) > 8 {
irc.nickcurrent = "_" + irc.nickcurrent
} else {
irc.nickcurrent = irc.nickcurrent + "_"
}
irc.SendRawf("NICK %s", irc.nickcurrent)
})
irc.AddCallback("PONG", func(e *Event) {
ns, _ := strconv.ParseInt(e.Message(), 10, 64)
delta := time.Duration(time.Now().UnixNano() - ns)
if irc.Debug {
irc.Log.Printf("Lag: %.3f s\n", delta.Seconds())
}
})
// NICK Define a nickname.
// Set irc.nickcurrent to the new nick actually used in this connection.
irc.AddCallback("NICK", func(e *Event) {
if e.Nick == irc.nick {
irc.nickcurrent = e.Message()
}
})
// 437: ERR_UNAVAILRESOURCE "<nick/channel> :Nick/channel is temporarily unavailable"
irc.AddCallback("433", irc.handleUnavailableNick)
irc.AddCallback("437", irc.handleUnavailableNick)
// 1: RPL_WELCOME "Welcome to the Internet Relay Network <nick>!<user>@<host>"
// Set irc.nickcurrent to the actually used nick in this connection.
irc.AddCallback("001", func(e *Event) {
irc.Lock()
irc.nickcurrent = e.Arguments[0]
irc.Unlock()
// Set irc.currentNick to the actually used nick in this connection.
irc.AddCallback("001", irc.handleRplWelcome)
// respond to NICK from the server (in response to our own NICK, or sent unprompted)
irc.AddCallback("NICK", func(e Event) {
if e.Nick() == irc.CurrentNick() && len(e.Params) > 0 {
irc.setCurrentNick(e.Params[0])
}
})
irc.AddCallback("ERROR", func(e Event) {
if !irc.isQuitting() {
irc.Log.Printf("ERROR received from server: %s", strings.Join(e.Params, " "))
}
})
irc.AddCallback("CAP", func(e Event) {
if len(e.Params) != 3 {
return
}
command := e.Params[1]
capsChan := irc.capsChan
// TODO this assumes all the caps on one line
// TODO support CAP LS 302
if command == "LS" {
capsList := strings.Fields(e.Params[2])
for _, capName := range irc.RequestCaps {
if sliceContains(capName, capsList) {
irc.Send("CAP", "REQ", capName)
} else {
select {
case capsChan <- capResult{capName, false}:
default:
}
}
}
} else if command == "ACK" || command == "NAK" {
for _, capName := range strings.Fields(e.Params[2]) {
select {
case capsChan <- capResult{capName, command == "ACK"}:
default:
}
}
}
})
if irc.UseSASL {
irc.setupSASLCallbacks()
}
if irc.EnableCTCP {
irc.setupCTCPCallbacks()
}
}
func (irc *Connection) handleRplWelcome(e Event) {
irc.stateMutex.Lock()
defer irc.stateMutex.Unlock()
// set the nickname we actually received from the server
if len(e.Params) > 0 {
irc.currentNick = e.Params[0]
}
// wake up Connect() if applicable
select {
case irc.welcomeChan <- empty{}:
default:
}
}
func (irc *Connection) handleUnavailableNick(e Event) {
// only try to change the nick if we're not registered yet,
// otherwise we'll change in response to pingLoop unsuccessfully
// trying to restore the intended nick (swapping one undesired nick
// for another)
var nickToTry string
irc.stateMutex.Lock()
if irc.currentNick == "" {
nickToTry = fmt.Sprintf("%s_%d", irc.Nick, irc.nickCounter)
irc.nickCounter++
}
irc.stateMutex.Unlock()
if nickToTry != "" {
irc.Send("NICK", nickToTry)
}
}

64
ircevent/irc_ctcp.go Normal file

@ -0,0 +1,64 @@
package ircevent
import (
"fmt"
"strings"
"time"
)
func eventRewriteCTCP(event *Event) {
// XXX rewrite event.Command for CTCP
if !(event.Command == "PRIVMSG" && len(event.Params) == 2 && strings.HasPrefix(event.Params[1], "\x01")) {
return
}
msg := event.Params[1]
event.Command = "CTCP" //Unknown CTCP
if i := strings.LastIndex(msg, "\x01"); i > 0 {
msg = msg[1:i]
} else {
return
}
if msg == "VERSION" {
event.Command = "CTCP_VERSION"
} else if msg == "TIME" {
event.Command = "CTCP_TIME"
} else if strings.HasPrefix(msg, "PING") {
event.Command = "CTCP_PING"
} else if msg == "USERINFO" {
event.Command = "CTCP_USERINFO"
} else if msg == "CLIENTINFO" {
event.Command = "CTCP_CLIENTINFO"
} else if strings.HasPrefix(msg, "ACTION") {
event.Command = "CTCP_ACTION"
if len(msg) > 6 {
msg = msg[7:]
} else {
msg = ""
}
}
event.Params[len(event.Params)-1] = msg
}
func (irc *Connection) setupCTCPCallbacks() {
irc.AddCallback("CTCP_VERSION", func(e Event) {
irc.SendRaw(fmt.Sprintf("NOTICE %s :\x01VERSION %s\x01", e.Nick(), irc.Version))
})
irc.AddCallback("CTCP_USERINFO", func(e Event) {
irc.SendRaw(fmt.Sprintf("NOTICE %s :\x01USERINFO %s\x01", e.Nick(), irc.User))
})
irc.AddCallback("CTCP_CLIENTINFO", func(e Event) {
irc.SendRaw(fmt.Sprintf("NOTICE %s :\x01CLIENTINFO PING VERSION TIME USERINFO CLIENTINFO\x01", e.Nick()))
})
irc.AddCallback("CTCP_TIME", func(e Event) {
irc.SendRaw(fmt.Sprintf("NOTICE %s :\x01TIME %s\x01", e.Nick(), time.Now().UTC().Format(time.RFC1123)))
})
irc.AddCallback("CTCP_PING", func(e Event) { irc.SendRaw(fmt.Sprintf("NOTICE %s :\x01%s\x01", e.Nick(), e.Message())) })
}

@ -1,46 +1,20 @@
package irc
package ircevent
import (
"testing"
)
func checkResult(t *testing.T, event *Event) {
if event.Nick != "nick" {
func TestParse(t *testing.T) {
event := new(Event)
event.Prefix = "nick!~user@host"
if event.Nick() != "nick" {
t.Fatal("Parse failed: nick")
}
if event.User != "~user" {
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)
t.Logf("%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")
if event.Host() != "host" {
t.Fatal("Parse failed: host")
}
}

@ -1,4 +1,4 @@
package irc
package ircevent
import (
"encoding/base64"
@ -7,11 +7,20 @@ import (
"strings"
)
type SASLResult struct {
type saslResult struct {
Failed bool
Err error
}
func sliceContains(str string, list []string) bool {
for _, x := range list {
if x == str {
return true
}
}
return false
}
// Check if a space-separated list of arguments contains a value.
func listContains(list string, value string) bool {
for _, arg_name := range strings.Split(strings.TrimSpace(list), " ") {
@ -22,42 +31,51 @@ func listContains(list string, value string) bool {
return false
}
func (irc *Connection) setupSASLCallbacks(result chan<- *SASLResult) {
irc.AddCallback("CAP", func(e *Event) {
if len(e.Arguments) == 3 {
if e.Arguments[1] == "LS" {
if !listContains(e.Arguments[2], "sasl") {
result <- &SASLResult{true, errors.New("no SASL capability " + e.Arguments[2])}
func (irc *Connection) submitSASLResult(r saslResult) {
select {
case irc.saslChan <- r:
default:
}
}
func (irc *Connection) setupSASLCallbacks() {
irc.AddCallback("CAP", func(e Event) {
if len(e.Params) == 3 {
if e.Params[1] == "LS" {
if !listContains(e.Params[2], "sasl") {
irc.submitSASLResult(saslResult{true, errors.New("no SASL capability " + e.Params[2])})
}
}
if e.Arguments[1] == "ACK" && listContains(e.Arguments[2], "sasl") {
if irc.SASLMech != "PLAIN" {
result <- &SASLResult{true, errors.New("only PLAIN is supported")}
}
irc.SendRaw("AUTHENTICATE " + irc.SASLMech)
if e.Params[1] == "ACK" && listContains(e.Params[2], "sasl") {
irc.Send("AUTHENTICATE", irc.SASLMech)
}
}
})
irc.AddCallback("AUTHENTICATE", func(e *Event) {
irc.AddCallback("AUTHENTICATE", func(e Event) {
str := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s\x00%s\x00%s", irc.SASLLogin, irc.SASLLogin, irc.SASLPassword)))
irc.SendRaw("AUTHENTICATE " + str)
irc.Send("AUTHENTICATE", str)
})
irc.AddCallback("901", func(e *Event) {
irc.AddCallback("901", func(e Event) {
irc.SendRaw("CAP END")
irc.SendRaw("QUIT")
result <- &SASLResult{true, errors.New(e.Arguments[1])}
irc.submitSASLResult(saslResult{true, errors.New(e.Params[1])})
})
irc.AddCallback("902", func(e *Event) {
irc.AddCallback("902", func(e Event) {
irc.SendRaw("CAP END")
irc.SendRaw("QUIT")
result <- &SASLResult{true, errors.New(e.Arguments[1])}
irc.submitSASLResult(saslResult{true, errors.New(e.Params[1])})
})
irc.AddCallback("903", func(e *Event) {
result <- &SASLResult{false, nil}
irc.AddCallback("903", func(e Event) {
irc.submitSASLResult(saslResult{false, nil})
})
irc.AddCallback("904", func(e *Event) {
irc.AddCallback("904", func(e Event) {
irc.SendRaw("CAP END")
irc.SendRaw("QUIT")
result <- &SASLResult{true, errors.New(e.Arguments[1])}
irc.submitSASLResult(saslResult{true, errors.New(e.Params[1])})
})
}

@ -1,43 +1,95 @@
package irc
package ircevent
import (
"crypto/tls"
"fmt"
"os"
"testing"
"time"
)
// set SASLLogin and SASLPassword environment variables before testing
func TestConnectionSASL(t *testing.T) {
SASLServer := "irc.freenode.net:7000"
SASLLogin := os.Getenv("SASLLogin")
SASLPassword := os.Getenv("SASLPassword")
const (
serverEnvVar = "IRCEVENT_SERVER"
saslEnvVar = "IRCEVENT_SASL_LOGIN"
saslPassVar = "IRCEVENT_SASL_PASSWORD"
)
if SASLLogin == "" {
t.Skip("Define SASLLogin and SASLPasword environment varables to test SASL")
func getSaslCreds() (account, password string) {
return os.Getenv(saslEnvVar), os.Getenv(saslPassVar)
}
func getenv(key, defaultValue string) (value string) {
value = os.Getenv(key)
if value == "" {
value = defaultValue
}
if testing.Short() {
t.Skip("skipping test in short mode.")
return
}
func getServer(sasl bool) string {
port := 6667
if sasl {
port = 6697
}
irccon := IRC("go-eventirc", "go-eventirc")
irccon.VerboseCallbackHandler = true
return fmt.Sprintf("%s:%d", getenv(serverEnvVar, "localhost"), port)
}
// set SASLLogin and SASLPassword environment variables before testing
func runCAPTest(caps []string, useSASL bool, t *testing.T) {
SASLLogin, SASLPassword := getSaslCreds()
if useSASL {
if SASLLogin == "" {
t.Skip("Define SASLLogin and SASLPasword environment varables to test SASL")
}
}
irccon := connForTesting("go-eventirc", "go-eventirc", true)
irccon.Debug = true
irccon.UseTLS = true
irccon.UseSASL = true
irccon.SASLLogin = SASLLogin
irccon.SASLPassword = SASLPassword
if useSASL {
irccon.UseSASL = true
irccon.SASLLogin = SASLLogin
irccon.SASLPassword = SASLPassword
}
irccon.RequestCaps = caps
irccon.TLSConfig = &tls.Config{InsecureSkipVerify: true}
irccon.AddCallback("001", func(e *Event) { irccon.Join("#go-eventirc") })
irccon.AddCallback("001", func(e Event) { irccon.Join("#go-eventirc") })
irccon.AddCallback("366", func(e *Event) {
irccon.AddCallback("366", func(e Event) {
irccon.Privmsg("#go-eventirc", "Test Message SASL\n")
time.Sleep(2 * time.Second)
irccon.Quit()
})
err := irccon.Connect(SASLServer)
err := irccon.Connect()
if err != nil {
t.Fatalf("SASL failed: %s", err)
}
irccon.Loop()
}
func TestConnectionSASL(t *testing.T) {
runCAPTest(nil, true, t)
}
func TestConnectionSASLWithAdditionalCaps(t *testing.T) {
runCAPTest([]string{"server-time", "message-tags", "batch", "labeled-response", "echo-message"}, true, t)
}
func TestConnectionSASLWithNonexistentCaps(t *testing.T) {
runCAPTest([]string{"server-time", "message-tags", "batch", "labeled-response", "echo-message", "oragono.io/xyzzy"}, true, t)
}
func TestConnectionSASLWithNonexistentCapsOnly(t *testing.T) {
runCAPTest([]string{"oragono.io/xyzzy"}, true, t)
}
func TestConnectionNonexistentCAPOnly(t *testing.T) {
runCAPTest([]string{"oragono.io/xyzzy"}, false, t)
}
func TestConnectionNonexistentCAPs(t *testing.T) {
runCAPTest([]string{"oragono.io/xyzzy", "server-time", "message-tags"}, false, t)
}
func TestConnectionGoodCAPs(t *testing.T) {
runCAPTest([]string{"server-time", "message-tags"}, false, t)
}

@ -2,92 +2,104 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package irc
package ircevent
import (
"context"
"crypto/tls"
"log"
"net"
"regexp"
"strings"
"sync"
"time"
"golang.org/x/text/encoding"
"github.com/goshuirc/irc-go/ircmsg"
)
type empty struct{}
type Callback func(Event)
type capResult struct {
capName string
ack bool
}
type Connection struct {
sync.Mutex
sync.WaitGroup
Debug bool
Error chan error
WebIRC string
Password string
UseTLS bool
UseSASL bool
RequestCaps []string
AcknowledgedCaps []string
SASLLogin string
SASLPassword string
SASLMech string
TLSConfig *tls.Config
Version string
Timeout time.Duration
CallbackTimeout time.Duration
PingFreq time.Duration
KeepAlive time.Duration
Server string
Encoding encoding.Encoding
// config data, user-settable
Server string
TLSConfig *tls.Config
Nick string
User string
RealName string // IRC realname/gecos
WebIRC []string // parameters for the WEBIRC command
Password string // server password (PASS command)
RequestCaps []string // IRCv3 capabilities to request (failure is non-fatal)
SASLLogin string // SASL credentials to log in with (failure is fatal)
SASLPassword string
SASLMech string
QuitMessage string
Version string
Timeout time.Duration
KeepAlive time.Duration
ReconnectFreq time.Duration
CallbackTimeout time.Duration
MaxLineLen int // maximum line length, not including tags
UseTLS bool
UseSASL bool
EnableCTCP bool
Debug bool
AllowPanic bool // if set, don't recover() from panics in callbacks
RealName string // The real name we want to display.
// If zero-value defaults to the user.
// networking and synchronization
stateMutex sync.Mutex // innermost mutex: don't block while holding this
end chan empty // closing this causes the goroutines to exit (think threading.Event)
pwrite chan []byte // receives IRC lines to be sent to the socket
wg sync.WaitGroup // after closing end, wait on this for all the goroutines to stop
socket net.Conn
lastError error
quitAt time.Time // time Quit() was called
running bool // is a connection active? is `end` open?
quit bool // user called Quit, do not reconnect
pingSent bool // we sent PING and are waiting for PONG
socket net.Conn
pwrite chan string
end chan struct{}
// IRC protocol connection state
currentNick string // nickname assigned by the server, empty before registration
acknowledgedCaps []string
nickCounter int
// Connect() builds these with sufficient capacity to receive all expected
// responses during negotiation. Sends to them are nonblocking, so anything
// sent outside of negotiation will not cause the relevant callbacks to block.
welcomeChan chan empty // signals that we got 001 and we are now connected
saslChan chan saslResult // transmits the final outcome of SASL negotiation
capsChan chan capResult // transmits the final status of each CAP negotiated
nick string //The nickname we want.
nickcurrent string //The nickname we currently have.
user string
registered bool
events map[string]map[int]func(*Event)
eventsMutex sync.Mutex
// callback state
eventsMutex sync.Mutex
events map[string]map[uint64]Callback
idCounter uint64 // assign unique IDs to callbacks
hasBaseCallbacks bool
QuitMessage string
lastMessage time.Time
lastMessageMutex sync.Mutex
VerboseCallbackHandler bool
Log *log.Logger
stopped bool
quit bool //User called Quit, do not reconnect.
Log *log.Logger
}
// A struct to represent an event.
type Event struct {
Code string
Raw string
Nick string //<nick>
Host string //<nick>!<usr>@<host>
Source string //<host>
User string //<usr>
Arguments []string
Tags map[string]string
Connection *Connection
Ctx context.Context
ircmsg.IRCMessage
Ctx context.Context
}
// Retrieve the last message from Event arguments.
// This function leaves the arguments untouched and
// returns an empty string if there are none.
func (e *Event) Message() string {
if len(e.Arguments) == 0 {
if len(e.Params) == 0 {
return ""
}
return e.Arguments[len(e.Arguments)-1]
return e.Params[len(e.Params)-1]
}
/*
// https://stackoverflow.com/a/10567935/6754440
// Regex of IRC formatting.
var ircFormat = regexp.MustCompile(`[\x02\x1F\x0F\x16\x1D\x1E]|\x03(\d\d?(,\d\d?)?)?`)
@ -101,3 +113,28 @@ func (e *Event) MessageWithoutFormat() string {
}
return ircFormat.ReplaceAllString(e.Arguments[len(e.Arguments)-1], "")
}
*/
func (e *Event) Nick() string {
nick, _, _ := e.splitNUH()
return nick
}
func (e *Event) User() string {
_, user, _ := e.splitNUH()
return user
}
func (e *Event) Host() string {
_, _, host := e.splitNUH()
return host
}
func (event *Event) splitNUH() (nick, user, host string) {
if i, j := strings.Index(event.Prefix, "!"), strings.Index(event.Prefix, "@"); i > -1 && j > -1 && i < j {
nick = event.Prefix[0:i]
user = event.Prefix[i+1 : j]
host = event.Prefix[j+1:]
}
return
}

@ -1,15 +1,16 @@
package irc
package ircevent
import (
"crypto/tls"
"math/rand"
"sort"
"strings"
"testing"
"time"
"github.com/goshuirc/irc-go/ircmsg"
)
const server = "irc.freenode.net:6667"
const serverssl = "irc.freenode.net:7000"
const channel = "#go-eventirc-test"
const dict = "abcdefghijklmnopqrstuvwxyz"
@ -17,99 +18,33 @@ const dict = "abcdefghijklmnopqrstuvwxyz"
const verbose_tests = false
const debug_tests = true
func TestConnectionEmtpyServer(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
err := irccon.Connect("")
if err == nil {
t.Fatal("emtpy server string not detected")
func connForTesting(nick, user string, tls bool) *Connection {
irc := &Connection{
Nick: nick,
User: user,
Server: getServer(tls),
}
return irc
}
func TestConnectionDoubleColon(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
err := irccon.Connect("::")
if err == nil {
t.Fatal("wrong number of ':' not detected")
}
}
func TestConnectionMissingHost(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
err := irccon.Connect(":6667")
if err == nil {
t.Fatal("missing host not detected")
}
}
func TestConnectionMissingPort(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
err := irccon.Connect("chat.freenode.net:")
if err == nil {
t.Fatal("missing port not detected")
}
}
func TestConnectionNegativePort(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
err := irccon.Connect("chat.freenode.net:-1")
if err == nil {
t.Fatal("negative port number not detected")
}
}
func TestConnectionTooLargePort(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
err := irccon.Connect("chat.freenode.net:65536")
if err == nil {
t.Fatal("too large port number not detected")
}
}
func TestConnectionMissingLog(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
irccon.Log = nil
err := irccon.Connect("chat.freenode.net:6667")
if err == nil {
t.Fatal("missing 'Log' not detected")
}
}
func TestConnectionEmptyUser(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
// user may be changed after creation
irccon.user = ""
err := irccon.Connect("chat.freenode.net:6667")
if err == nil {
t.Fatal("empty 'user' not detected")
}
}
func TestConnectionEmptyNick(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
// nick may be changed after creation
irccon.nick = ""
err := irccon.Connect("chat.freenode.net:6667")
if err == nil {
t.Fatal("empty 'nick' not detected")
}
func mockEvent(command string) ircmsg.IRCMessage {
return ircmsg.MakeMessage(nil, ":server.name", command)
}
func TestRemoveCallback(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
irccon := connForTesting("go-eventirc", "go-eventirc", false)
debugTest(irccon)
done := make(chan int, 10)
irccon.AddCallback("TEST", func(e *Event) { done <- 1 })
id := irccon.AddCallback("TEST", func(e *Event) { done <- 2 })
irccon.AddCallback("TEST", func(e *Event) { done <- 3 })
irccon.AddCallback("TEST", func(e Event) { done <- 1 })
id := irccon.AddCallback("TEST", func(e Event) { done <- 2 })
irccon.AddCallback("TEST", func(e Event) { done <- 3 })
// Should remove callback at index 1
irccon.RemoveCallback("TEST", id)
irccon.RemoveCallback(id)
irccon.RunCallbacks(&Event{
Code: "TEST",
})
irccon.runCallbacks(mockEvent("TEST"))
var results []int
@ -122,17 +57,15 @@ func TestRemoveCallback(t *testing.T) {
}
func TestWildcardCallback(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
irccon := connForTesting("go-eventirc", "go-eventirc", false)
debugTest(irccon)
done := make(chan int, 10)
irccon.AddCallback("TEST", func(e *Event) { done <- 1 })
irccon.AddCallback("*", func(e *Event) { done <- 2 })
irccon.AddCallback("TEST", func(e Event) { done <- 1 })
irccon.AddCallback("*", func(e Event) { done <- 2 })
irccon.RunCallbacks(&Event{
Code: "TEST",
})
irccon.runCallbacks(mockEvent("TEST"))
var results []int
@ -145,20 +78,18 @@ func TestWildcardCallback(t *testing.T) {
}
func TestClearCallback(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc")
irccon := connForTesting("go-eventirc", "go-eventirc", false)
debugTest(irccon)
done := make(chan int, 10)
irccon.AddCallback("TEST", func(e *Event) { done <- 0 })
irccon.AddCallback("TEST", func(e *Event) { done <- 1 })
irccon.AddCallback("TEST", func(e Event) { done <- 0 })
irccon.AddCallback("TEST", func(e Event) { done <- 1 })
irccon.ClearCallback("TEST")
irccon.AddCallback("TEST", func(e *Event) { done <- 2 })
irccon.AddCallback("TEST", func(e *Event) { done <- 3 })
irccon.AddCallback("TEST", func(e Event) { done <- 2 })
irccon.AddCallback("TEST", func(e Event) { done <- 3 })
irccon.RunCallbacks(&Event{
Code: "TEST",
})
irccon.runCallbacks(mockEvent("TEST"))
var results []int
@ -171,7 +102,7 @@ func TestClearCallback(t *testing.T) {
}
func TestIRCemptyNick(t *testing.T) {
irccon := IRC("", "go-eventirc")
irccon := connForTesting("", "go-eventirc", false)
irccon = nil
if irccon != nil {
t.Error("empty nick didn't result in error")
@ -179,12 +110,6 @@ func TestIRCemptyNick(t *testing.T) {
}
}
func TestIRCemptyUser(t *testing.T) {
irccon := IRC("go-eventirc", "")
if irccon != nil {
t.Error("empty user didn't result in error")
}
}
func TestConnection(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
@ -192,22 +117,20 @@ func TestConnection(t *testing.T) {
rand.Seed(time.Now().UnixNano())
ircnick1 := randStr(8)
ircnick2 := randStr(8)
irccon1 := IRC(ircnick1, "IRCTest1")
irccon1.PingFreq = time.Second * 3
ircnick2orig := ircnick2
irccon1 := connForTesting(ircnick1, "IRCTest1", false)
debugTest(irccon1)
irccon2 := IRC(ircnick2, "IRCTest2")
irccon2 := connForTesting(ircnick2, "IRCTest2", false)
debugTest(irccon2)
teststr := randStr(20)
testmsgok := make(chan bool, 1)
irccon1.AddCallback("001", func(e *Event) { irccon1.Join(channel) })
irccon2.AddCallback("001", func(e *Event) { irccon2.Join(channel) })
irccon1.AddCallback("366", func(e *Event) {
go func(e *Event) {
irccon1.AddCallback("001", func(e Event) { irccon1.Join(channel) })
irccon2.AddCallback("001", func(e Event) { irccon2.Join(channel) })
irccon1.AddCallback("366", func(e Event) {
go func(e Event) {
tick := time.NewTicker(1 * time.Second)
i := 10
for {
@ -229,14 +152,14 @@ func TestConnection(t *testing.T) {
}(e)
})
irccon2.AddCallback("366", func(e *Event) {
irccon2.AddCallback("366", func(e Event) {
ircnick2 = randStr(8)
irccon2.Nick(ircnick2)
irccon2.SetNick(ircnick2)
})
irccon2.AddCallback("PRIVMSG", func(e *Event) {
irccon2.AddCallback("PRIVMSG", func(e Event) {
if e.Message() == teststr {
if e.Nick == ircnick1 {
if e.Nick() == ircnick1 {
testmsgok <- true
irccon2.Quit()
} else {
@ -248,18 +171,18 @@ func TestConnection(t *testing.T) {
}
})
irccon2.AddCallback("NICK", func(e *Event) {
if irccon2.nickcurrent == ircnick2 {
irccon2.AddCallback("NICK", func(e Event) {
if !(e.Nick() == ircnick2orig && e.Message() == ircnick2) {
t.Errorf("Nick change did not work!")
}
})
err := irccon1.Connect(server)
err := irccon1.Connect()
if err != nil {
t.Log(err.Error())
t.Errorf("Can't connect to freenode.")
}
err = irccon2.Connect(server)
err = irccon2.Connect()
if err != nil {
t.Log(err.Error())
t.Errorf("Can't connect to freenode.")
@ -269,31 +192,40 @@ func TestConnection(t *testing.T) {
irccon1.Loop()
}
func TestReconnect(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
func runReconnectTest(useSASL bool, t *testing.T) {
ircnick1 := randStr(8)
irccon := IRC(ircnick1, "IRCTestRe")
irccon.PingFreq = time.Second * 3
irccon := connForTesting(ircnick1, "IRCTestRe", false)
irccon.ReconnectFreq = time.Second * 1
saslLogin, saslPassword := getSaslCreds()
if useSASL {
if saslLogin == "" {
t.Skip("Define SASL environment varables to test SASL")
} else {
irccon.UseSASL = true
irccon.SASLLogin = saslLogin
irccon.SASLPassword = saslPassword
}
}
debugTest(irccon)
connects := 0
irccon.AddCallback("001", func(e *Event) { irccon.Join(channel) })
irccon.AddCallback("001", func(e Event) { irccon.Join(channel) })
irccon.AddCallback("366", func(e *Event) {
irccon.AddCallback("366", func(e Event) {
connects += 1
if connects > 2 {
irccon.Privmsgf(channel, "Connection nr %d (test done)\n", connects)
go irccon.Quit()
} else {
irccon.Privmsgf(channel, "Connection nr %d\n", connects)
time.Sleep(100) //Need to let the thraed actually send before closing socket
go irccon.Disconnect()
// XXX: wait for the message to actually send before we hang up
// (can this be avoided?)
time.Sleep(100 * time.Millisecond)
go irccon.Reconnect()
}
})
err := irccon.Connect(server)
err := irccon.Connect()
if err != nil {
t.Log(err.Error())
t.Errorf("Can't connect to freenode.")
@ -305,23 +237,31 @@ func TestReconnect(t *testing.T) {
}
}
func TestReconnect(t *testing.T) {
runReconnectTest(false, t)
}
func TestReconnectWithSASL(t *testing.T) {
runReconnectTest(true, t)
}
func TestConnectionSSL(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
ircnick1 := randStr(8)
irccon := IRC(ircnick1, "IRCTestSSL")
irccon := connForTesting(ircnick1, "IRCTestSSL", true)
debugTest(irccon)
irccon.UseTLS = true
irccon.TLSConfig = &tls.Config{InsecureSkipVerify: true}
irccon.AddCallback("001", func(e *Event) { irccon.Join(channel) })
irccon.AddCallback("001", func(e Event) { irccon.Join(channel) })
irccon.AddCallback("366", func(e *Event) {
irccon.AddCallback("366", func(e Event) {
irccon.Privmsg(channel, "Test Message from SSL\n")
irccon.Quit()
})
err := irccon.Connect(serverssl)
err := irccon.Connect()
if err != nil {
t.Log(err.Error())
t.Errorf("Can't connect to freenode.")
@ -340,7 +280,6 @@ func randStr(n int) string {
}
func debugTest(irccon *Connection) *Connection {
irccon.VerboseCallbackHandler = verbose_tests
irccon.Debug = debug_tests
return irccon
}
@ -358,3 +297,45 @@ func compareResults(received []int, desired ...int) bool {
}
return true
}
func TestConnectionNickInUse(t *testing.T) {
rand.Seed(time.Now().UnixNano())
ircnick := randStr(8)
irccon1 := connForTesting(ircnick, "IRCTest1", false)
debugTest(irccon1)
irccon2 := connForTesting(ircnick, "IRCTest2", false)
debugTest(irccon2)
n1 := make(chan string, 1)
n2 := make(chan string, 1)
// check the actual nick after 001 is processed
irccon1.AddCallback("002", func(e Event) { n1 <- irccon1.CurrentNick() })
irccon2.AddCallback("002", func(e Event) { n2 <- irccon2.CurrentNick() })
err := irccon1.Connect()
if err != nil {
panic(err)
}
err = irccon2.Connect()
if err != nil {
panic(err)
}
go irccon2.Loop()
go irccon1.Loop()
nick1 := <-n1
nick2 := <-n2
irccon1.Quit()
irccon2.Quit()
// we should have gotten two different nicks, one a prefix of the other
if nick1 == ircnick && len(nick1) < len(nick2) && strings.HasPrefix(nick2, nick1) {
return
}
if nick2 == ircnick && len(nick2) < len(nick1) && strings.HasPrefix(nick1, nick2) {
return
}
t.Errorf("expected %s and a suffixed version, got %s and %s", ircnick, nick1, nick2)
}

@ -1,14 +0,0 @@
// +build gofuzz
package irc
func Fuzz(data []byte) int {
b := bytes.NewBuffer(data)
event, err := parseToEvent(b.String())
if err == nil {
irc := IRC("go-eventirc", "go-eventirc")
irc.RunCallbacks(event)
return 1
}
return 0
}