package irc import ( "bufio" "fmt" "log" "net" "strconv" "strings" "time" "unicode" ) const ( //IrcVersion exports the current version of the irc lib IrcVersion = "0.1.1776" ) // Connection holds the callbacks for the client type Connection struct { Sock net.Conn DirectCallback func(string, string) PrivmsgCallback func(string, string, string) JoinCallback func(string, string) QuitCallback func(string, string) PartCallback func(string, string, string) NickCallback func(string, string) NumericCallback func(string, int, string) PrivmsgCallbackEx func(*Connection, string, string, string) JoinCallbackEx func(*Connection, string, string) QuitCallbackEx func(*Connection, string, string) PartCallbackEx func(*Connection, string, string, string) joined bool channels []string curNick string userList map[string][]string } func cleanInput(msg string) string { ret := msg if strings.Contains(msg, "\n") { ret = strings.ReplaceAll(msg, "\n", "-") } if strings.Contains(ret, "\r") { ret = strings.ReplaceAll(msg, "\r", "") } return ret } // SendRaw sends a string as a raw irc message func (c *Connection) SendRaw(msg string) error { _, err := c.Sock.Write([]byte(msg)) return err } // SendPong replies to the received PING func (c *Connection) SendPong(ping string) { pong := strings.Replace(ping, "PING", "PONG", 1) c.SendRaw(pong + "\n") } // SendPrivmsg sends a privmsg to channel/target func (c *Connection) SendPrivmsg(channel, msg string) { msg = cleanInput(msg) _, err := c.Sock.Write([]byte(fmt.Sprintf("PRIVMSG %s :%s\n", channel, msg))) if err != nil { log.Printf("Error sending private message: %s\n", err.Error()) } } // SendNotice sends a notice to channel/target func (c *Connection) SendNotice(channel, msg string) { msg = cleanInput(msg) _, err := c.Sock.Write([]byte(fmt.Sprintf("NOTICE %s :%s\n", channel, msg))) if err != nil { log.Printf("Error sending NOTICE message: %s\n", err.Error()) } } // SendNick changes the clients nick func (c *Connection) SendNick(nick string) { c.Sock.Write([]byte(fmt.Sprintf("NICK %s\n", nick))) } // SendJoin sends a JOIN func (c *Connection) SendJoin(channel string) { c.Sock.Write([]byte(fmt.Sprintf("JOIN %s\n", channel))) } // SendPart sends a PART func (c *Connection) SendPart(channel, msg string) { c.Sock.Write([]byte(fmt.Sprintf("PART %s :%s\n", channel, msg))) } // SendQuit sends a QUIT func (c *Connection) SendQuit(msg string) { c.Sock.Write([]byte(fmt.Sprintf("QUIT :%s\n", msg))) } // SendNames sends a NAMES request for the channel func (c *Connection) SendNames(channel string) { c.Sock.Write([]byte(fmt.Sprintf("NAMES %s\n", channel))) } // SendPass sends a PASS for network func (c *Connection) SendPass(pass string) { c.Sock.Write([]byte(fmt.Sprintf("PASS %s\n", pass))) } // SendUser sends a USER command for the network func (c *Connection) SendUser(user, realname string) { c.Sock.Write([]byte(fmt.Sprintf("USER %s * * :%s\n", user, realname))) } // DelayedSend sends a privmsg AFTER the specified delay func (c *Connection) DelayedSend(sock net.Conn, dur time.Duration, channel, msg string) { time.Sleep(dur) c.SendPrivmsg(channel, msg) } // GetNicks returns a array of all nicks in a channel func (c *Connection) GetNicks(channel string) []string { return c.userList[channel] } // Kill terminates the event loop of connection func (c *Connection) Kill() { c.Sock.Close() } // NewConnection creates and connects an Connection func NewConnection(server, nick, user string, pass *string, chans []string) *Connection { irc := &Connection{channels: chans, userList: map[string][]string{}} sock, err := net.Dial("tcp", server) if err != nil { log.Fatalln(err.Error()) } irc.Sock = sock if pass != nil { irc.SendPass(*pass) } irc.SendNick(nick) irc.curNick = nick irc.SendUser(user, user) return irc } // GetNick gets the nickname portion of a host string func GetNick(name string) string { return strings.Split(name, "!")[0] } func hasNick(nick string, names []string) bool { for _, v := range names { if v == nick { return true } } return false } func hasOpSymbol(nick string) bool { s := rune(nick[0]) if !unicode.IsLetter(s) && !unicode.IsNumber(s) { return true } return false } func (c *Connection) updateNicks(channel string, names []string) { //log.Printf("updating channel %s with %d nicks\n", channel, len(names)) for _, i := range names { c.addNick(channel, i) } } func (c *Connection) addNick(channel string, nick string) { channel = strings.ToLower(channel) if nick != c.curNick && !hasNick(nick, c.userList[channel]) { if hasOpSymbol(nick) { nick = nick[1:] } //log.Printf("added nick: %s to channel %s\n", nick, channel) c.userList[channel] = append(c.userList[channel], nick) } } func (c *Connection) removeNick(channel string, nick string) { channel = strings.ToLower(channel) nicks := []string{} for _, i := range c.userList[channel] { if i != nick { nicks = append(nicks, i) } } //log.Printf("removed nick: %s from channel %s\n", nick, channel) c.userList[channel] = nicks } func (c *Connection) removeNickAllChans(nick string) { for k := range c.userList { c.removeNick(k, nick) } } func (c *Connection) renameNick(old, nick string) { for channel, i := range c.userList { if hasNick(old, i) { c.removeNick(channel, old) c.addNick(channel, nick) } } } // TODO: simplify this to pass gocyclo func (c *Connection) parseMessage(line string) { if line[0] == ':' { buf := line[1:] params := strings.SplitN(buf, " ", 3) from := params[0] cmd := params[1] args := params[2] if code, e := strconv.Atoi(cmd); e == nil { //numeric message if !c.joined { if code == RPL_ENDOFMOTD { for _, v := range c.channels { c.SendJoin(v) } c.joined = true } } if code == RPL_NAMREPLY { params := strings.SplitN(args, " ", 4) target := strings.TrimSpace(params[2]) c.updateNicks(target, strings.Split(params[3][1:], " ")) } if c.NumericCallback != nil { code, _ := strconv.Atoi(cmd) c.NumericCallback(from, code, args) } } else { t := strings.SplitN(args, ":", 2) target := strings.TrimSpace(t[0]) msg := "" if len(t) > 1 { msg = t[1] } switch strings.ToLower(cmd) { case "privmsg": if target != c.curNick { if c.PrivmsgCallback != nil { c.PrivmsgCallback(target, from, msg) } if c.PrivmsgCallbackEx != nil { c.PrivmsgCallbackEx(c, target, from, msg) } } else { if c.DirectCallback != nil { c.DirectCallback(from, msg) } } case "join": c.addNick(msg, GetNick(from)) if c.JoinCallback != nil { c.JoinCallback(from, msg) } if c.JoinCallbackEx != nil { c.JoinCallbackEx(c, from, msg) } case "quit": c.removeNickAllChans(GetNick(from)) if c.QuitCallback != nil { c.QuitCallback(from, msg) } if c.QuitCallbackEx != nil { c.QuitCallbackEx(c, from, msg) } case "part": c.removeNick(target, GetNick(from)) if c.PartCallback != nil { c.PartCallback(from, target, msg) } if c.PartCallbackEx != nil { c.PartCallbackEx(c, from, target, msg) } case "nick": if GetNick(from) == c.curNick { c.curNick = msg log.Printf("BOT NICK CHANGED TO: %s\n", c.curNick) } if c.NickCallback != nil { c.NickCallback(from, msg) } default: //log.Printf("unhandled command: %s %s %s", cmd, target, msg) } } } } // Run runs the irc event loop func (c *Connection) Run() { rdr := bufio.NewScanner(c.Sock) c.joined = false for rdr.Scan() { line := rdr.Text() if strings.HasPrefix(line, "PING") { c.SendPong(line) continue } c.parseMessage(line) } }