pax_global_header00006660000000000000000000000064132626620730014521gustar00rootroot0000000000000052 comment=5063e5f26097f6423c9de665079347c9473788da go-ircevent-0.2/000077500000000000000000000000001326266207300136045ustar00rootroot00000000000000go-ircevent-0.2/LICENSE000066400000000000000000000030231326266207300146070ustar00rootroot00000000000000// Copyright (c) 2009 Thomas Jager. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. go-ircevent-0.2/README.markdown000066400000000000000000000040601326266207300163050ustar00rootroot00000000000000Description ----------- Event based irc client library. 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 Example ------- See [examples/simple/simple.go](examples/simple/simple.go) and [irc_test.go](irc_test.go) 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 +Many more 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) }); Commands -------- ircobj := irc.IRC("", "") //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("") //sends string to server. Adds \r\n ircobj.SendRawf("", ...) //sends formatted string to server.n ircobj.Join("<#channel> [password]") ircobj.Nick("newnick") ircobj.Privmsg("", "msg") // sends a message to either a certain nick or a channel ircobj.Privmsgf(, "", ...) ircobj.Notice("", "msg") ircobj.Noticef("", "", ...) go-ircevent-0.2/examples/000077500000000000000000000000001326266207300154225ustar00rootroot00000000000000go-ircevent-0.2/examples/simple/000077500000000000000000000000001326266207300167135ustar00rootroot00000000000000go-ircevent-0.2/examples/simple/simple.go000066400000000000000000000012521326266207300205330ustar00rootroot00000000000000package main import ( "github.com/thoj/go-ircevent" "crypto/tls" "fmt" ) const channel = "#go-eventirc-test"; const serverssl = "irc.freenode.net:7000" 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) if err != nil { fmt.Printf("Err %s", err ) return } irccon.Loop() } go-ircevent-0.2/irc.go000066400000000000000000000367571326266207300147320ustar00rootroot00000000000000// Copyright 2009 Thomas Jager All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. /* This package provides an event based IRC client library. It allows to register callbacks for the events you need to handle. Its features include handling standard CTCP, reconnecting on errors and detecting stones servers. Details of the IRC protocol can be found in the following RFCs: https://tools.ietf.org/html/rfc1459 https://tools.ietf.org/html/rfc2810 https://tools.ietf.org/html/rfc2811 https://tools.ietf.org/html/rfc2812 https://tools.ietf.org/html/rfc2813 The details of the client-to-client protocol (CTCP) can be found here: http://www.irchelp.org/irchelp/rfc/ctcpspec.html */ package irc import ( "bufio" "bytes" "crypto/tls" "errors" "fmt" "log" "net" "os" "strconv" "strings" "time" ) const ( VERSION = "go-ircevent v2.1" ) var ErrDisconnected = errors.New("Disconnect Called") // Read data from a connection. To be used as a goroutine. func (irc *Connection) readLoop() { defer irc.Done() br := bufio.NewReaderSize(irc.socket, 512) errChan := irc.ErrorChan() for { select { case <-irc.end: return default: // Set a read deadline based on the combined timeout and ping frequency // We should ALWAYS have received a response from the server within the timeout // after our own pings if irc.socket != nil { irc.socket.SetReadDeadline(time.Now().Add(irc.Timeout + irc.PingFreq)) } msg, err := br.ReadString('\n') // We got past our blocking read, so bin timeout if irc.socket != nil { var zero time.Time irc.socket.SetReadDeadline(zero) } if err != nil { errChan <- err return } if irc.Debug { irc.Log.Printf("<-- %s\n", strings.TrimSpace(msg)) } irc.lastMessageMutex.Lock() irc.lastMessage = time.Now() irc.lastMessageMutex.Unlock() event, err := parseToEvent(msg) event.Connection = irc if err == nil { /* XXX: len(args) == 0: args should be empty */ irc.RunCallbacks(event) } } } } // Unescape tag values as defined in the IRCv3.2 message tags spec // http://ircv3.net/specs/core/message-tags-3.2.html func unescapeTagValue(value string) string { value = strings.Replace(value, "\\:", ";", -1) value = strings.Replace(value, "\\s", " ", -1) value = strings.Replace(value, "\\\\", "\\", -1) value = strings.Replace(value, "\\r", "\r", -1) value = strings.Replace(value, "\\n", "\n", -1) return value } //Parse raw irc messages func parseToEvent(msg string) (*Event, error) { msg = strings.TrimSuffix(msg, "\n") //Remove \r\n msg = strings.TrimSuffix(msg, "\r") event := &Event{Raw: msg} if len(msg) < 5 { return nil, errors.New("Malformed msg from server") } if msg[0] == '@' { // IRCv3 Message Tags if i := strings.Index(msg, " "); i > -1 { event.Tags = make(map[string]string) tags := strings.Split(msg[1:i], ";") for _, data := range tags { parts := strings.SplitN(data, "=", 2) if len(parts) == 1 { event.Tags[parts[0]] = "" } else { event.Tags[parts[0]] = unescapeTagValue(parts[1]) } } msg = msg[i+1 : len(msg)] } else { return nil, errors.New("Malformed msg from server") } } if msg[0] == ':' { if i := strings.Index(msg, " "); i > -1 { event.Source = msg[1:i] msg = msg[i+1 : len(msg)] } else { return nil, errors.New("Malformed msg from server") } if i, j := strings.Index(event.Source, "!"), strings.Index(event.Source, "@"); i > -1 && j > -1 && i < j { event.Nick = event.Source[0:i] event.User = event.Source[i+1 : j] event.Host = event.Source[j+1 : len(event.Source)] } } split := strings.SplitN(msg, " :", 2) args := strings.Split(split[0], " ") event.Code = strings.ToUpper(args[0]) event.Arguments = args[1:] if len(split) > 1 { event.Arguments = append(event.Arguments, split[1]) } return event, nil } // Loop to write to a connection. To be used as a goroutine. func (irc *Connection) writeLoop() { defer irc.Done() errChan := irc.ErrorChan() for { select { case <-irc.end: return case b, ok := <-irc.pwrite: if !ok || b == "" || irc.socket == nil { return } if irc.Debug { irc.Log.Printf("--> %s\n", strings.TrimSpace(b)) } // Set a write deadline based on the time out irc.socket.SetWriteDeadline(time.Now().Add(irc.Timeout)) _, err := irc.socket.Write([]byte(b)) // Past blocking write, bin timeout var zero time.Time irc.socket.SetWriteDeadline(zero) if err != nil { errChan <- err return } } } } // Pings the server if we have not received any messages for 5 minutes // to keep the connection alive. To be used as a goroutine. func (irc *Connection) pingLoop() { defer irc.Done() ticker := time.NewTicker(1 * time.Minute) // Tick every minute for monitoring ticker2 := time.NewTicker(irc.PingFreq) // Tick at the ping frequency. for { select { case <-ticker.C: //Ping if we haven't received anything from the server within the keep alive period irc.lastMessageMutex.Lock() if time.Since(irc.lastMessage) >= irc.KeepAlive { irc.SendRawf("PING %d", time.Now().UnixNano()) } irc.lastMessageMutex.Unlock() case <-ticker2.C: //Ping at the ping frequency irc.SendRawf("PING %d", time.Now().UnixNano()) //Try to recapture nickname if it's not as configured. irc.Lock() if irc.nick != irc.nickcurrent { irc.nickcurrent = irc.nick irc.SendRawf("NICK %s", irc.nick) } irc.Unlock() case <-irc.end: ticker.Stop() ticker2.Stop() return } } } func (irc *Connection) isQuitting() bool { irc.Lock() defer irc.Unlock() return irc.quit } // Main loop to control the connection. func (irc *Connection) Loop() { errChan := irc.ErrorChan() for !irc.isQuitting() { err := <-errChan close(irc.end) irc.Wait() for !irc.isQuitting() { irc.Log.Printf("Error, disconnected: %s\n", err) if err = irc.Reconnect(); err != nil { irc.Log.Printf("Error while reconnecting: %s\n", err) time.Sleep(60 * time.Second) } else { errChan = irc.ErrorChan() break } } } } // Quit the current connection and disconnect from the server // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1.6 func (irc *Connection) Quit() { quit := "QUIT" if irc.QuitMessage != "" { quit = fmt.Sprintf("QUIT :%s", irc.QuitMessage) } irc.SendRaw(quit) irc.Lock() irc.stopped = true irc.quit = true irc.Unlock() } // Use the connection to join a given channel. // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.1 func (irc *Connection) Join(channel string) { irc.pwrite <- fmt.Sprintf("JOIN %s\r\n", channel) } // Leave a given channel. // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.2 func (irc *Connection) Part(channel string) { irc.pwrite <- fmt.Sprintf("PART %s\r\n", channel) } // Send a notification to a nickname. This is similar to Privmsg but must not receive replies. // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.2 func (irc *Connection) Notice(target, message string) { irc.pwrite <- fmt.Sprintf("NOTICE %s :%s\r\n", target, message) } // Send a formated notification to a nickname. // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.2 func (irc *Connection) Noticef(target, format string, a ...interface{}) { irc.Notice(target, fmt.Sprintf(format, a...)) } // Send (action) message to a target (channel or nickname). // No clear RFC on this one... func (irc *Connection) Action(target, message string) { irc.pwrite <- fmt.Sprintf("PRIVMSG %s :\001ACTION %s\001\r\n", target, message) } // Send formatted (action) message to a target (channel or nickname). func (irc *Connection) Actionf(target, format string, a ...interface{}) { irc.Action(target, fmt.Sprintf(format, a...)) } // Send (private) message to a target (channel or nickname). // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.1 func (irc *Connection) Privmsg(target, message string) { irc.pwrite <- fmt.Sprintf("PRIVMSG %s :%s\r\n", target, message) } // Send formated string to specified target (channel or nickname). func (irc *Connection) Privmsgf(target, format string, a ...interface{}) { irc.Privmsg(target, fmt.Sprintf(format, a...)) } // Kick from with . For no message, pass empty string ("") func (irc *Connection) Kick(user, channel, msg string) { var cmd bytes.Buffer cmd.WriteString(fmt.Sprintf("KICK %s %s", channel, user)) if msg != "" { cmd.WriteString(fmt.Sprintf(" :%s", msg)) } cmd.WriteString("\r\n") irc.pwrite <- cmd.String() } // Kick all from with . For no message, pass // empty string ("") func (irc *Connection) MultiKick(users []string, channel string, msg string) { var cmd bytes.Buffer cmd.WriteString(fmt.Sprintf("KICK %s %s", channel, strings.Join(users, ","))) if msg != "" { cmd.WriteString(fmt.Sprintf(" :%s", msg)) } cmd.WriteString("\r\n") irc.pwrite <- cmd.String() } // Send raw string. func (irc *Connection) SendRaw(message string) { irc.pwrite <- message + "\r\n" } // Send raw formated string. func (irc *Connection) SendRawf(format string, a ...interface{}) { irc.SendRaw(fmt.Sprintf(format, a...)) } // Set (new) nickname. // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1.2 func (irc *Connection) Nick(n string) { irc.nick = n irc.SendRawf("NICK %s", n) } // Determine nick currently used with the connection. func (irc *Connection) GetNick() string { return irc.nickcurrent } // Query information about a particular nickname. // RFC 1459: https://tools.ietf.org/html/rfc1459#section-4.5.2 func (irc *Connection) Whois(nick string) { irc.SendRawf("WHOIS %s", nick) } // Query information about a given nickname in the server. // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.5.1 func (irc *Connection) Who(nick string) { irc.SendRawf("WHO %s", nick) } // Set different modes for a target (channel or nickname). // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.3 func (irc *Connection) Mode(target string, modestring ...string) { if len(modestring) > 0 { mode := strings.Join(modestring, " ") irc.SendRawf("MODE %s %s", target, mode) return } irc.SendRawf("MODE %s", target) } func (irc *Connection) ErrorChan() chan error { return irc.Error } // Returns true if the connection is connected to an IRC server. func (irc *Connection) Connected() bool { return !irc.stopped } // A disconnect sends all buffered messages (if possible), // stops all goroutines and then closes the socket. func (irc *Connection) Disconnect() { irc.Lock() defer irc.Unlock() if irc.end != nil { close(irc.end) } irc.end = nil if irc.pwrite != nil { close(irc.pwrite) } irc.Wait() if irc.socket != nil { irc.socket.Close() } irc.ErrorChan() <- ErrDisconnected } // Reconnect to a server using the current connection. func (irc *Connection) Reconnect() error { irc.end = make(chan struct{}) return irc.Connect(irc.Server) } // Connect to a given server using the current connection configuration. // This function also takes care of identification if a password is provided. // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1 func (irc *Connection) Connect(server string) error { irc.Server = server // mark Server as stopped since there can be an error during connect irc.stopped = true // make sure everything is ready for connection if len(irc.Server) == 0 { return errors.New("empty 'server'") } if strings.Count(irc.Server, ":") != 1 { return errors.New("wrong number of ':' in address") } if strings.Index(irc.Server, ":") == 0 { return errors.New("hostname is missing") } if strings.Index(irc.Server, ":") == len(irc.Server)-1 { return errors.New("port missing") } // check for valid range ports := strings.Split(irc.Server, ":")[1] port, err := strconv.Atoi(ports) if err != nil { return errors.New("extracting port failed") } if !((port >= 0) && (port <= 65535)) { return errors.New("port number outside valid range") } if irc.Log == nil { return errors.New("'Log' points to nil") } if len(irc.nick) == 0 { return errors.New("empty 'nick'") } if len(irc.user) == 0 { return errors.New("empty 'user'") } if irc.UseTLS { dialer := &net.Dialer{Timeout: irc.Timeout} irc.socket, err = tls.DialWithDialer(dialer, "tcp", irc.Server, irc.TLSConfig) } else { irc.socket, err = net.DialTimeout("tcp", irc.Server, irc.Timeout) } if err != nil { return err } irc.stopped = false irc.Log.Printf("Connected to %s (%s)\n", irc.Server, irc.socket.RemoteAddr()) irc.pwrite = make(chan string, 10) irc.Error = make(chan error, 2) irc.Add(3) go irc.readLoop() go irc.writeLoop() go irc.pingLoop() if len(irc.WebIRC) > 0 { irc.pwrite <- fmt.Sprintf("WEBIRC %s\r\n", irc.WebIRC) } if len(irc.Password) > 0 { irc.pwrite <- fmt.Sprintf("PASS %s\r\n", irc.Password) } err = irc.negotiateCaps() if err != nil { return err } realname := irc.user if irc.RealName != "" { realname = irc.RealName } irc.pwrite <- fmt.Sprintf("NICK %s\r\n", irc.nick) irc.pwrite <- fmt.Sprintf("USER %s 0.0.0.0 0.0.0.0 :%s\r\n", irc.user, realname) return nil } // Negotiate IRCv3 capabilities func (irc *Connection) negotiateCaps() error { saslResChan := make(chan *SASLResult) if irc.UseSASL { irc.RequestCaps = append(irc.RequestCaps, "sasl") irc.setupSASLCallbacks(saslResChan) } if len(irc.RequestCaps) == 0 { return nil } cap_chan := make(chan bool, len(irc.RequestCaps)) irc.AddCallback("CAP", func(e *Event) { if len(e.Arguments) != 3 { return } command := e.Arguments[1] if command == "LS" { missing_caps := len(irc.RequestCaps) for _, cap_name := range strings.Split(e.Arguments[2], " ") { for _, req_cap := range irc.RequestCaps { if cap_name == req_cap { irc.pwrite <- fmt.Sprintf("CAP REQ :%s\r\n", cap_name) missing_caps-- } } } for i := 0; i < missing_caps; i++ { cap_chan <- true } } else if command == "ACK" || command == "NAK" { for _, cap_name := range strings.Split(strings.TrimSpace(e.Arguments[2]), " ") { if cap_name == "" { continue } if command == "ACK" { irc.AcknowledgedCaps = append(irc.AcknowledgedCaps, cap_name) } cap_chan <- true } } }) irc.pwrite <- "CAP LS\r\n" if irc.UseSASL { select { case res := <-saslResChan: if res.Failed { close(saslResChan) return res.Err } case <-time.After(time.Second * 15): close(saslResChan) return errors.New("SASL setup timed out. This shouldn't happen.") } } // Wait for all capabilities to be ACKed or NAKed before ending negotiation for i := 0; i < len(irc.RequestCaps); i++ { <-cap_chan } irc.pwrite <- fmt.Sprintf("CAP END\r\n") realname := irc.user if irc.RealName != "" { realname = irc.RealName } irc.pwrite <- fmt.Sprintf("NICK %s\r\n", irc.nick) irc.pwrite <- fmt.Sprintf("USER %s 0.0.0.0 0.0.0.0 :%s\r\n", irc.user, realname) return nil } // Create a connection with the (publicly visible) nickname and username. // The nickname is later used to address the user. Returns nil if nick // or user are empty. func IRC(nick, user string) *Connection { // catch invalid values if len(nick) == 0 { return nil } if len(user) == 0 { return nil } irc := &Connection{ nick: nick, nickcurrent: nick, user: user, Log: log.New(os.Stdout, "", log.LstdFlags), end: make(chan struct{}), Version: VERSION, KeepAlive: 4 * time.Minute, Timeout: 1 * time.Minute, PingFreq: 15 * time.Minute, SASLMech: "PLAIN", QuitMessage: "", } irc.setupCallbacks() return irc } go-ircevent-0.2/irc_callback.go000066400000000000000000000146251326266207300165340ustar00rootroot00000000000000package irc import ( "strconv" "strings" "time" ) // 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. 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 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]) } irc.events[eventcode][id] = callback irc.eventsMutex.Unlock() 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) 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 } // 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 { 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 } // 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) 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) } irc.Log.Printf("Event not found. Use AddCallBack\n") } // 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 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 msg == "VERSION" { event.Code = "CTCP_VERSION" } else if msg == "TIME" { event.Code = "CTCP_TIME" } 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 = "" } } event.Arguments[len(event.Arguments)-1] = msg } irc.eventsMutex.Lock() callbacks, ok := irc.events[event.Code] irc.eventsMutex.Unlock() if ok { if irc.VerboseCallbackHandler { irc.Log.Printf("%v (%v) >> %#v\n", event.Code, len(callbacks), event) } for _, callback := range callbacks { callback(event) } } else if irc.VerboseCallbackHandler { irc.Log.Printf("%v (0) >> %#v\n", event.Code, event) } irc.eventsMutex.Lock() allcallbacks, ok := irc.events["*"] irc.eventsMutex.Unlock() if ok { if irc.VerboseCallbackHandler { irc.Log.Printf("%v (0) >> %#v\n", event.Code, event) } for _, callback := range allcallbacks { callback(event) } } } // Set up some initial callbacks to handle the IRC/CTCP protocol. func (irc *Connection) setupCallbacks() { irc.events = make(map[string]map[int]func(*Event)) //Handle ping events irc.AddCallback("PING", func(e *Event) { irc.SendRaw("PONG :" + e.Message()) }) //Version handler irc.AddCallback("CTCP_VERSION", func(e *Event) { irc.SendRawf("NOTICE %s :\x01VERSION %s\x01", e.Nick, irc.Version) }) 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 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) }) // 433: ERR_NICKNAMEINUSE " :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() } }) // 1: RPL_WELCOME "Welcome to the Internet Relay Network !@" // 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() }) } go-ircevent-0.2/irc_parse_test.go000066400000000000000000000020551326266207300171430ustar00rootroot00000000000000package irc import ( "fmt" "testing" ) func checkResult(t *testing.T, event *Event) { if event.Nick != "nick" { t.Fatal("Parse failed: nick") } if event.User != "~user" { t.Fatal("Parse failed: user") } if event.Code != "PRIVMSG" { t.Fatal("Parse failed: code") } if event.Arguments[0] != "#channel" { t.Fatal("Parse failed: channel") } if event.Arguments[1] != "message text" { t.Fatal("Parse failed: message") } } func TestParse(t *testing.T) { event, err := parseToEvent(":nick!~user@host PRIVMSG #channel :message text") if err != nil { t.Fatal("Parse PRIVMSG failed") } checkResult(t, event) } func TestParseTags(t *testing.T) { event, err := parseToEvent("@tag;+tag2=raw+:=,escaped\\:\\s\\\\ :nick!~user@host PRIVMSG #channel :message text") if err != nil { t.Fatal("Parse PRIVMSG with tags failed") } checkResult(t, event) fmt.Printf("%s", event.Tags) if _, ok := event.Tags["tag"]; !ok { t.Fatal("Parsing value-less tag failed") } if event.Tags["+tag2"] != "raw+:=,escaped; \\" { t.Fatal("Parsing tag failed") } } go-ircevent-0.2/irc_sasl.go000066400000000000000000000025741326266207300157420ustar00rootroot00000000000000package irc import ( "encoding/base64" "errors" "fmt" "strings" ) type SASLResult struct { Failed bool Err error } func (irc *Connection) setupSASLCallbacks(result chan<- *SASLResult) { irc.AddCallback("CAP", func(e *Event) { if len(e.Arguments) == 3 { if e.Arguments[1] == "LS" { if !strings.Contains(e.Arguments[2], "sasl") { result <- &SASLResult{true, errors.New("no SASL capability " + e.Arguments[2])} } } if e.Arguments[1] == "ACK" { if irc.SASLMech != "PLAIN" { result <- &SASLResult{true, errors.New("only PLAIN is supported")} } irc.SendRaw("AUTHENTICATE " + irc.SASLMech) } } }) 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.AddCallback("901", func(e *Event) { irc.SendRaw("CAP END") irc.SendRaw("QUIT") result <- &SASLResult{true, errors.New(e.Arguments[1])} }) irc.AddCallback("902", func(e *Event) { irc.SendRaw("CAP END") irc.SendRaw("QUIT") result <- &SASLResult{true, errors.New(e.Arguments[1])} }) irc.AddCallback("903", func(e *Event) { result <- &SASLResult{false, nil} }) irc.AddCallback("904", func(e *Event) { irc.SendRaw("CAP END") irc.SendRaw("QUIT") result <- &SASLResult{true, errors.New(e.Arguments[1])} }) } go-ircevent-0.2/irc_sasl_test.go000066400000000000000000000017561326266207300170020ustar00rootroot00000000000000package irc import ( "crypto/tls" "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") if SASLLogin == "" { t.Skip("Define SASLLogin and SASLPasword environment varables to test SASL") } irccon := IRC("go-eventirc", "go-eventirc") irccon.VerboseCallbackHandler = true irccon.Debug = true irccon.UseTLS = true irccon.UseSASL = true irccon.SASLLogin = SASLLogin irccon.SASLPassword = SASLPassword irccon.TLSConfig = &tls.Config{InsecureSkipVerify: true} irccon.AddCallback("001", func(e *Event) { irccon.Join("#go-eventirc") }) 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) if err != nil { t.Fatalf("SASL failed: %s", err) } irccon.Loop() } go-ircevent-0.2/irc_struct.go000066400000000000000000000035471326266207300163250ustar00rootroot00000000000000// Copyright 2009 Thomas Jager All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package irc import ( "crypto/tls" "log" "net" "sync" "time" ) 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 PingFreq time.Duration KeepAlive time.Duration Server string RealName string // The real name we want to display. // If zero-value defaults to the user. socket net.Conn pwrite chan string end chan struct{} 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 QuitMessage string lastMessage time.Time lastMessageMutex sync.Mutex VerboseCallbackHandler bool Log *log.Logger stopped bool quit bool //User called Quit, do not reconnect. } // A struct to represent an event. type Event struct { Code string Raw string Nick string // Host string //!@ Source string // User string // Arguments []string Tags map[string]string Connection *Connection } // 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 { return "" } return e.Arguments[len(e.Arguments)-1] } go-ircevent-0.2/irc_test.go000066400000000000000000000167431326266207300157620ustar00rootroot00000000000000package irc import ( "crypto/tls" "math/rand" "testing" "time" ) const server = "irc.freenode.net:6667" const serverssl = "irc.freenode.net:7000" const channel = "#go-eventirc-test" const dict = "abcdefghijklmnopqrstuvwxyz" //Spammy 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 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 TestRemoveCallback(t *testing.T) { irccon := IRC("go-eventirc", "go-eventirc") 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 }) // Should remove callback at index 1 irccon.RemoveCallback("TEST", id) irccon.RunCallbacks(&Event{ Code: "TEST", }) var results []int results = append(results, <-done) results = append(results, <-done) if len(results) != 2 || results[0] == 2 || results[1] == 2 { t.Error("Callback 2 not removed") } } func TestWildcardCallback(t *testing.T) { irccon := IRC("go-eventirc", "go-eventirc") debugTest(irccon) done := make(chan int, 10) irccon.AddCallback("TEST", func(e *Event) { done <- 1 }) irccon.AddCallback("*", func(e *Event) { done <- 2 }) irccon.RunCallbacks(&Event{ Code: "TEST", }) var results []int results = append(results, <-done) results = append(results, <-done) if len(results) != 2 || !(results[0] == 1 && results[1] == 2) { t.Error("Wildcard callback not called") } } func TestClearCallback(t *testing.T) { irccon := IRC("go-eventirc", "go-eventirc") debugTest(irccon) done := make(chan int, 10) 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.RunCallbacks(&Event{ Code: "TEST", }) var results []int results = append(results, <-done) results = append(results, <-done) if len(results) != 2 || !(results[0] == 2 && results[1] == 3) { t.Error("Callbacks not cleared") } } func TestIRCemptyNick(t *testing.T) { irccon := IRC("", "go-eventirc") irccon = nil if irccon != nil { t.Error("empty nick didn't result in error") t.Fail() } } 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) { rand.Seed(time.Now().UnixNano()) ircnick1 := randStr(8) ircnick2 := randStr(8) irccon1 := IRC(ircnick1, "IRCTest1") irccon1.PingFreq = time.Second * 3 debugTest(irccon1) irccon2 := IRC(ircnick2, "IRCTest2") 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) { tick := time.NewTicker(1 * time.Second) i := 10 for { select { case <-tick.C: irccon1.Privmsgf(channel, "%s\n", teststr) if i == 0 { t.Errorf("Timeout while wating for test message from the other thread.") return } case <-testmsgok: tick.Stop() irccon1.Quit() return } i -= 1 } }(e) }) irccon2.AddCallback("366", func(e *Event) { ircnick2 = randStr(8) irccon2.Nick(ircnick2) }) irccon2.AddCallback("PRIVMSG", func(e *Event) { if e.Message() == teststr { if e.Nick == ircnick1 { testmsgok <- true irccon2.Quit() } else { t.Errorf("Test message came from an unexpected nickname") } } else { //this may fail if there are other incoming messages, unlikely. t.Errorf("Test message mismatch") } }) irccon2.AddCallback("NICK", func(e *Event) { if irccon2.nickcurrent == ircnick2 { t.Errorf("Nick change did not work!") } }) err := irccon1.Connect(server) if err != nil { t.Log(err.Error()) t.Errorf("Can't connect to freenode.") } err = irccon2.Connect(server) if err != nil { t.Log(err.Error()) t.Errorf("Can't connect to freenode.") } go irccon2.Loop() irccon1.Loop() } func TestReconnect(t *testing.T) { ircnick1 := randStr(8) irccon := IRC(ircnick1, "IRCTestRe") debugTest(irccon) connects := 0 irccon.AddCallback("001", func(e *Event) { irccon.Join(channel) }) irccon.AddCallback("366", func(e *Event) { connects += 1 if connects > 2 { irccon.Privmsgf(channel, "Connection nr %d (test done)\n", connects) irccon.Quit() } else { irccon.Privmsgf(channel, "Connection nr %d\n", connects) time.Sleep(100) //Need to let the thraed actually send before closing socket irccon.Disconnect() } }) err := irccon.Connect(server) if err != nil { t.Log(err.Error()) t.Errorf("Can't connect to freenode.") } irccon.Loop() if connects != 3 { t.Errorf("Reconnect test failed. Connects = %d", connects) } } func TestConnectionSSL(t *testing.T) { ircnick1 := randStr(8) irccon := IRC(ircnick1, "IRCTestSSL") debugTest(irccon) irccon.UseTLS = true irccon.TLSConfig = &tls.Config{InsecureSkipVerify: true} irccon.AddCallback("001", func(e *Event) { irccon.Join(channel) }) irccon.AddCallback("366", func(e *Event) { irccon.Privmsg(channel, "Test Message from SSL\n") irccon.Quit() }) err := irccon.Connect(serverssl) if err != nil { t.Log(err.Error()) t.Errorf("Can't connect to freenode.") } irccon.Loop() } // Helper Functions func randStr(n int) string { b := make([]byte, n) for i := range b { b[i] = dict[rand.Intn(len(dict))] } return string(b) } func debugTest(irccon *Connection) *Connection { irccon.VerboseCallbackHandler = verbose_tests irccon.Debug = debug_tests return irccon } go-ircevent-0.2/irc_test_fuzz.go000066400000000000000000000003601326266207300170240ustar00rootroot00000000000000// +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 }