/***************************************************************************** * Copyright (c) 2009, Kenneth Falck http://kfalck.net * 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 Kenneth Falck nor the names of his 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 HOLDER 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. *****************************************************************************/ package irc import "os" import "fmt" import "net" import "io" import "bufio" import "strings" import "regexp" /**************************************************************************************************** * Generic utilities ****************************************************************************************************/ func StripNewlines(line string) string { // Strip trailing newline chars for strings.HasSuffix(line, "\n") || strings.HasSuffix(line, "\r") { line = line[0:len(line)-1] } return line; } /**************************************************************************************************** * IRC server configuration. ****************************************************************************************************/ type ServerConfig struct { Host string; // Server hostname Port uint; // Server port Password string; // Server password Nickname string; // Nickname Username string; // Username Realname string; // Realname } /**************************************************************************************************** * IRC protocol line. ****************************************************************************************************/ type IRCName struct { Nickname string; // NICK!user@host Username string; // nick!USER@host Hostname string; // nick!user@HOST } func (name *IRCName) String() string { s := name.Nickname; if len(name.Username) > 0 { s += "!" + name.Username + "@" + name.Hostname; } return s; } var namere *regexp.Regexp = nil; func NewIRCName(unparsed string) *IRCName { if namere == nil { namere, _ = regexp.Compile("^([^!@]*)!([^@]*)@(.*)$"); } nick, user, host := "", "", ""; matches := namere.ExecuteString(unparsed); if len(matches) > 0 { // Full name nick = unparsed[matches[2]:matches[3]]; user = unparsed[matches[4]:matches[5]]; host = unparsed[matches[6]:matches[7]]; } else { // Only nickname nick = unparsed; } return &IRCName{nick, user, host}; } type IRCLine struct { Conn *IRCConn; // Associated connection or nil if local. Prefix string; // The :xxx prefix Cmd string; // Command or response code Args []string; // Arguments, where last one is the :final arg } func (line *IRCLine) String() string { s := ""; if len(line.Prefix) > 0 { s += ":" + line.Prefix + " "; } s += line.Cmd; for i, arg := range(line.Args) { s += " "; if i == len(line.Args)-1 { s += ":"; } s += arg; } return s; } func (line *IRCLine) IsFromSelf() bool { return line.Conn != nil && len(line.Prefix) > 0 && NewIRCName(line.Prefix).Nickname == line.Conn.config.Nickname; } func (line *IRCLine) IsToSelf() bool { return line.Conn != nil && len(line.Args) > 0 && line.Conn.config.Nickname == line.Args[0]; } func NewIRCLine(unparsed string) *IRCLine { // Match [:prefix ]cmd arg2 arg3[ :finalarg] prefix, cmd, finalarg := "", "", ""; args := []string{}; if len(unparsed) <= 0 { return &IRCLine{Prefix:cmd, Cmd:cmd, Args:args}; } if unparsed[0] == ':' { parts := strings.Split(unparsed, " ", 2); prefix = strings.TrimSpace(parts[0][1:len(parts[0])]); if len(parts) > 1 { unparsed = strings.TrimSpace(parts[1]); } else { unparsed = ""; } } finalpos := strings.Index(unparsed, ":"); if finalpos == -1 { cmd = strings.TrimSpace(unparsed); } else { cmd = strings.TrimSpace(unparsed[0:finalpos]); finalarg = unparsed[finalpos+1:len(unparsed)]; } args = strings.Split(cmd, " ", 0); if len(finalarg) > 0 { nargs := make([]string, len(args)+1, len(args)+1); for i, arg := range args { nargs[i] = arg; } nargs[len(nargs)-1] = finalarg; args = nargs; } _ = finalarg; return &IRCLine{Prefix:prefix, Cmd:args[0], Args:args[1:len(args)]}; } /**************************************************************************************************** * IRC server connection. ****************************************************************************************************/ type IRCConn struct { client *IRCClient; // Owning IRC client. config *ServerConfig; // Configuration for this connection conn net.Conn; // TCP server connection } func (ic *IRCConn) SendCommand(cmd string, args ...) os.Error { buf := fmt.Sprintf(cmd, args) + "\r\n"; _, err := io.WriteString(ic.conn, buf); return err; } func (ic *IRCConn) Close() (err os.Error) { err = ic.conn.Close(); return; } func (ic *IRCConn) IRCBackgroundProcess() { var line string; var err os.Error; reader := bufio.NewReader(ic.conn); for line, err = reader.ReadString('\n'); err == nil; line, err = reader.ReadString('\n') { ircLine := NewIRCLine(StripNewlines(line)); ircLine.Conn = ic; ic.client.receive <- ircLine; } // Connection terminated ic.client.disconnect <- err; } func NewIRCConnection(client *IRCClient, config *ServerConfig) (ic *IRCConn, err os.Error) { ic = &IRCConn{client:client, config:config}; conn, err := net.Dial("tcp", "", fmt.Sprintf("%s:%d", config.Host, config.Port)); if conn == nil { return nil, err; } // Successful connect, create channels ic.conn = conn; //ic.conn.SetTimeout(60000000); // Log in to server if len(ic.config.Password) > 0 { ic.SendCommand("PASS :%s", ic.config.Password); } ic.SendCommand("NICK %s", ic.config.Nickname); ic.SendCommand("USER %s %s %s :%s", ic.config.Username, ic.config.Username, ic.config.Host, ic.config.Realname); // Start background process go ic.IRCBackgroundProcess(); return ic, err } /**************************************************************************************************** * IRC client, can connect to multiple servers. ****************************************************************************************************/ type IRCHandler struct { match *regexp.Regexp; // Response matched by handler. handler func (*IRCLine) bool; // Callback function. } type IRCCmdHandler struct { match *regexp.Regexp; // Command matched by handler. handler func (args []string) bool; // Callback function. } type IRCClient struct { conns []*IRCConn; // Server connections. input chan string; // Keyboard input channel. currentConn int; // Current connection, index into conns. responseHandlers []*IRCHandler; // Response handler functions. commandHandlers []*IRCCmdHandler; // Command handler functions. receive chan *IRCLine; // Channel for receiving server responses disconnect chan os.Error; // Channel for receiving connection termination signal CurrentChannel string; // Current channel name or empty. } func (client *IRCClient) AddServer(config *ServerConfig) os.Error { ic, err := NewIRCConnection(client, config); if ic == nil { return err; } client.conns = client.conns[0:len(client.conns)+1]; client.conns[len(client.conns)-1] = ic; return nil; } func (client *IRCClient) SendCommand(cmd string, args ...) os.Error { return client.conns[client.currentConn].SendCommand(cmd, args); } func (client *IRCClient) JoinChannel(channel string) { client.SendCommand("JOIN %s", channel); } func (client *IRCClient) LeaveChannel(channel string) { client.SendCommand("PART %s", channel); } func (client *IRCClient) PrivMsg(receiver string, msg string) { client.SendCommand("PRIVMSG %s :%s", receiver, msg); } func (client *IRCClient) AddResponseHandler(match string, handler func (line *IRCLine) bool) { re, _ := regexp.Compile(match); if len(client.responseHandlers) == cap(client.responseHandlers) { newlist := make([]*IRCHandler, len(client.responseHandlers), len(client.responseHandlers)+1); for i, item := range client.responseHandlers { newlist[i] = item; } client.responseHandlers = newlist; } client.responseHandlers = client.responseHandlers[0:len(client.responseHandlers)+1]; client.responseHandlers[len(client.responseHandlers)-1] = &IRCHandler{match:re, handler:handler}; } func (client *IRCClient) AddCommandHandler(match string, handler func (args []string) bool) { re, _ := regexp.Compile(match); lastIndex := len(client.commandHandlers); client.commandHandlers = client.commandHandlers[0:lastIndex+1]; client.commandHandlers[lastIndex] = &IRCCmdHandler{match:re, handler:handler}; } func (client *IRCClient) DispatchResponse(ircLine *IRCLine) { cmd := strings.ToUpper(ircLine.Cmd); for _, handler := range client.responseHandlers { if handler.match.MatchString(cmd) { if !handler.handler(ircLine) { break } } } } func (client *IRCClient) DispatchError(format string, args ...) { client.DispatchResponse(&IRCLine{Prefix:"", Cmd:"INTERNAL:ERROR", Args:[]string{fmt.Sprintf(format, args)}}); } func (client *IRCClient) DispatchMessage(format string, args ...) { client.DispatchResponse(&IRCLine{Prefix:"", Cmd:"INTERNAL:MESSAGE", Args:[]string{fmt.Sprintf(format, args)}}); } func (client *IRCClient) Run() { var line string; var ircLine *IRCLine; var err os.Error; // Register built-in command and response handlers. client.AddCommandHandler("quit", func (args []string) bool { client.SendCommand("QUIT :%s", "Bye-bye"); return false; }); client.AddCommandHandler("join", func (args []string) bool { if len(args) < 1 { client.DispatchError("*** Usage: /join [key]"); } else { param := args[0]; for i, arg := range args { if i > 0 { param += " " + arg; } } client.SendCommand("JOIN %s", param); } return true; }); client.AddCommandHandler("leave|part", func (args []string) bool { if len(args) < 1 { client.DispatchError("*** Usage: /leave "); } else { client.SendCommand("PART %s", args[0]); } return true; }); client.AddCommandHandler("msg", func (args []string) bool { if len(args) < 2 { client.DispatchError("*** Usage: /msg message"); } else { msg := args[1]; for i, arg := range args { if i > 1 { msg += " " + arg; } } client.SendCommand("PRIVMSG %s :%s", args[0], msg); } return true; }); client.AddResponseHandler("JOIN", func (line *IRCLine) bool { if line.IsFromSelf() { client.CurrentChannel = line.Args[0]; client.DispatchMessage("*** Now talking to channel %s", client.CurrentChannel); } return true; }); client.AddResponseHandler("PART", func (line *IRCLine) bool { if line.IsFromSelf() { client.CurrentChannel = ""; client.DispatchMessage("*** No longer talking to channel %s", client.CurrentChannel); } return true; }); client.AddResponseHandler("PRIVMSG", func (line *IRCLine) bool { return true; }); client.AddResponseHandler("PING", func (line *IRCLine) bool { client.SendCommand("PONG :%s", client.conns[client.currentConn].config.Nickname); return true; }); // Main loop, listen to all go-channels. connection: for { select { case line = <-client.input: if len(line) > 0 { if line[0] == '/' { args := strings.Split(line[1:len(line)], " ", 0); cmd := strings.ToLower(args[0]); for _, handler := range client.commandHandlers { if handler.match.MatchString(cmd) { if !handler.handler(args[1:len(args)]) { break connection } } } } else if len(client.CurrentChannel) > 0 { client.SendCommand("PRIVMSG %s :%s", client.CurrentChannel, line); client.DispatchMessage("<%s> %s", client.conns[client.currentConn].config.Nickname, line); } else { client.DispatchError("Cannot send, not on any channel."); } } case ircLine = <-client.receive: client.DispatchResponse(ircLine); case err = <-client.disconnect: // Server connection lost. client.DispatchError("*** Server connection closed: %s", err.String()); _ = err; } } for connIndex := range client.conns { client.conns[connIndex].Close(); } } func NewIRCClient(input chan string) *IRCClient { client := &IRCClient{conns:make([]*IRCConn, 0, 100), input:input, responseHandlers:make([]*IRCHandler, 0), commandHandlers:make([]*IRCCmdHandler, 0, 100), receive:make(chan *IRCLine), disconnect:make(chan os.Error)}; return client; }