initial fixes and refactoring
This commit is contained in:
parent
9d5dccb821
commit
dc36bd80a6
7
Makefile
7
Makefile
@ -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()
|
||||
}
|
||||
|
68
ircevent/examples/stress.go
Normal file
68
ircevent/examples/stress.go
Normal file
@ -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()
|
||||
}
|
909
ircevent/irc.go
909
ircevent/irc.go
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
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
|
||||
}
|
Loading…
Reference in New Issue
Block a user