2020-01-05 12:39:58 -05:00
|
|
|
package irc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"net"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
2020-09-01 22:18:43 -04:00
|
|
|
"unicode"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
//IrcVersion exports the current version of the irc lib
|
|
|
|
IrcVersion = "0.1.0"
|
2020-01-05 12:39:58 -05:00
|
|
|
)
|
|
|
|
|
2020-01-05 12:48:57 -05:00
|
|
|
//Connection holds the callbacks for the client
|
|
|
|
type Connection struct {
|
2020-01-05 12:39:58 -05:00
|
|
|
Sock net.Conn
|
2020-01-05 23:19:14 -05:00
|
|
|
DirectCallback func(string, string)
|
2020-01-05 13:04:31 -05:00
|
|
|
PrivmsgCallback func(string, string, string)
|
|
|
|
JoinCallback func(string, string)
|
|
|
|
QuitCallback func(string, string)
|
|
|
|
PartCallback func(string, string, string)
|
2020-01-05 21:20:25 -05:00
|
|
|
NickCallback func(string, string)
|
2020-01-05 13:04:31 -05:00
|
|
|
NumericCallback func(string, int, string)
|
2020-01-05 12:39:58 -05:00
|
|
|
joined bool
|
|
|
|
channels []string
|
2020-01-05 22:07:58 -05:00
|
|
|
curNick string
|
2020-01-05 22:28:08 -05:00
|
|
|
userList map[string][]string
|
2020-01-05 12:39:58 -05:00
|
|
|
}
|
|
|
|
|
2020-01-06 16:26:46 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-01-05 17:45:55 -05:00
|
|
|
//SendPong replies to the received PING
|
2020-01-05 12:48:57 -05:00
|
|
|
func (c *Connection) SendPong(ping string) {
|
2020-01-05 12:39:58 -05:00
|
|
|
pong := strings.Replace(ping, "PING", "PONG", 1)
|
|
|
|
c.Sock.Write([]byte(fmt.Sprintf("%s\n", pong)))
|
|
|
|
log.Println("pong sent")
|
|
|
|
}
|
|
|
|
|
2020-01-05 12:48:57 -05:00
|
|
|
//SendPrivmsg sends a privmsg to channel/target
|
|
|
|
func (c *Connection) SendPrivmsg(channel, msg string) {
|
2020-01-06 16:26:46 -05:00
|
|
|
msg = cleanInput(msg)
|
2020-01-05 12:39:58 -05:00
|
|
|
_, 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())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-05 12:48:57 -05:00
|
|
|
//SendNotice sends a notice to channel/target
|
|
|
|
func (c *Connection) SendNotice(channel, msg string) {
|
2020-01-06 16:26:46 -05:00
|
|
|
msg = cleanInput(msg)
|
2020-01-05 12:39:58 -05:00
|
|
|
_, 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())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-05 12:48:57 -05:00
|
|
|
//SendNick changes the clients nick
|
|
|
|
func (c *Connection) SendNick(nick string) {
|
2020-01-05 12:39:58 -05:00
|
|
|
c.Sock.Write([]byte(fmt.Sprintf("NICK %s\n", nick)))
|
|
|
|
}
|
|
|
|
|
2020-01-05 12:48:57 -05:00
|
|
|
//SendJoin sends a JOIN
|
|
|
|
func (c *Connection) SendJoin(channel string) {
|
2020-01-05 12:39:58 -05:00
|
|
|
c.Sock.Write([]byte(fmt.Sprintf("JOIN %s\n", channel)))
|
|
|
|
}
|
|
|
|
|
2020-01-05 12:48:57 -05:00
|
|
|
//SendPart sends a PART
|
|
|
|
func (c *Connection) SendPart(channel, msg string) {
|
2020-01-05 12:39:58 -05:00
|
|
|
c.Sock.Write([]byte(fmt.Sprintf("PART %s :%s\n", channel, msg)))
|
|
|
|
}
|
|
|
|
|
2020-01-05 12:48:57 -05:00
|
|
|
//SendQuit sends a QUIT
|
|
|
|
func (c *Connection) SendQuit(msg string) {
|
2020-01-05 12:39:58 -05:00
|
|
|
c.Sock.Write([]byte(fmt.Sprintf("QUIT :%s\n", msg)))
|
|
|
|
}
|
|
|
|
|
2020-01-05 12:48:57 -05:00
|
|
|
//SendNames sends a NAMES request for the channel
|
|
|
|
func (c *Connection) SendNames(channel string) {
|
|
|
|
c.Sock.Write([]byte(fmt.Sprintf("NAMES %s\n", channel)))
|
2020-01-05 12:39:58 -05:00
|
|
|
}
|
|
|
|
|
2020-01-05 12:48:57 -05:00
|
|
|
//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)
|
2020-01-05 12:39:58 -05:00
|
|
|
}
|
|
|
|
|
2020-01-05 12:48:57 -05:00
|
|
|
//NewConnection creates and connects an Connection
|
2020-09-01 22:50:39 -04:00
|
|
|
func NewConnection(server, nick, user string, pass *string, chans []string) *Connection {
|
2020-01-05 23:19:14 -05:00
|
|
|
irc := &Connection{channels: chans, userList: map[string][]string{}}
|
2020-01-05 12:39:58 -05:00
|
|
|
|
|
|
|
sock, err := net.Dial("tcp", server)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalln(err.Error())
|
|
|
|
}
|
|
|
|
irc.Sock = sock
|
|
|
|
irc.Sock.Write([]byte(fmt.Sprintf("USER %s * * :%s\n", user, user)))
|
|
|
|
irc.SendNick(nick)
|
2020-01-05 22:07:58 -05:00
|
|
|
irc.curNick = nick
|
2020-09-01 22:50:39 -04:00
|
|
|
if pass != nil {
|
|
|
|
irc.Sock.Write([]byte(fmt.Sprintf("PASS %s\n", *pass)))
|
|
|
|
}
|
2020-01-05 12:39:58 -05:00
|
|
|
return irc
|
|
|
|
}
|
|
|
|
|
2020-01-05 14:18:27 -05:00
|
|
|
//GetNick gets the nickname portion of a host string
|
|
|
|
func GetNick(name string) string {
|
2020-01-05 12:39:58 -05:00
|
|
|
return strings.Split(name, "!")[0]
|
|
|
|
}
|
|
|
|
|
2020-01-06 19:53:32 -05:00
|
|
|
func hasNick(nick string, names []string) bool {
|
|
|
|
for _, v := range names {
|
|
|
|
if v == nick {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-09-01 22:18:43 -04:00
|
|
|
func hasOpSymbol(nick string) bool {
|
|
|
|
s := rune(nick[0])
|
|
|
|
if !unicode.IsLetter(s) && !unicode.IsNumber(s) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-01-06 19:45:31 -05:00
|
|
|
func (c *Connection) updateNicks(channel string, names []string) {
|
2020-01-06 19:48:17 -05:00
|
|
|
log.Printf("updating channel %s with %d nicks\n", channel, len(names))
|
2020-01-06 19:45:31 -05:00
|
|
|
for _, i := range names {
|
2020-09-01 22:18:43 -04:00
|
|
|
c.addNick(channel, i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Connection) addNick(channel string, nick string) {
|
|
|
|
if !hasNick(nick, c.userList[channel]) {
|
|
|
|
if hasOpSymbol(nick) {
|
|
|
|
nick = nick[1:]
|
2020-01-06 19:45:31 -05:00
|
|
|
}
|
2020-09-01 22:18:43 -04:00
|
|
|
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) {
|
|
|
|
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)
|
2020-01-06 19:45:31 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-05 17:00:14 -05:00
|
|
|
//TODO: simplify this to pass gocyclo
|
2020-01-05 12:48:57 -05:00
|
|
|
func (c *Connection) parseMessage(line string) {
|
2020-01-05 12:39:58 -05:00
|
|
|
if line[0] == ':' {
|
|
|
|
buf := line[1:]
|
|
|
|
params := strings.SplitN(buf, " ", 3)
|
|
|
|
from := params[0]
|
|
|
|
cmd := params[1]
|
|
|
|
args := params[2]
|
|
|
|
|
|
|
|
if _, e := strconv.Atoi(cmd); e == nil {
|
|
|
|
//numeric message
|
|
|
|
if !c.joined {
|
|
|
|
if cmd == "376" {
|
|
|
|
for _, v := range c.channels {
|
|
|
|
c.SendJoin(v)
|
|
|
|
}
|
|
|
|
c.joined = true
|
|
|
|
}
|
|
|
|
}
|
2020-01-05 22:28:08 -05:00
|
|
|
|
|
|
|
if cmd == "353" {
|
2020-09-01 22:18:43 -04:00
|
|
|
idx := strings.Index(args, "=") + 2
|
2020-01-05 22:59:29 -05:00
|
|
|
s := strings.Split(args[idx:], ":")
|
2020-09-01 22:18:43 -04:00
|
|
|
target := strings.TrimSpace(strings.ToLower(s[0]))
|
|
|
|
c.updateNicks(target, strings.Split(s[1], " "))
|
2020-01-05 22:28:08 -05:00
|
|
|
}
|
|
|
|
|
2020-01-05 13:04:31 -05:00
|
|
|
if c.NumericCallback != nil {
|
2020-01-05 12:39:58 -05:00
|
|
|
code, _ := strconv.Atoi(cmd)
|
2020-01-05 13:04:31 -05:00
|
|
|
c.NumericCallback(from, code, args)
|
2020-01-05 12:39:58 -05:00
|
|
|
}
|
|
|
|
} 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":
|
2020-01-05 23:19:14 -05:00
|
|
|
if target != c.curNick {
|
|
|
|
if c.PrivmsgCallback != nil {
|
|
|
|
c.PrivmsgCallback(target, from, msg)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if c.DirectCallback != nil {
|
|
|
|
c.DirectCallback(from, msg)
|
|
|
|
}
|
2020-01-05 12:39:58 -05:00
|
|
|
}
|
|
|
|
case "join":
|
2020-09-01 22:18:43 -04:00
|
|
|
c.addNick(target, GetNick(from))
|
2020-01-05 13:04:31 -05:00
|
|
|
if c.JoinCallback != nil {
|
|
|
|
c.JoinCallback(from, target)
|
2020-01-05 12:39:58 -05:00
|
|
|
}
|
|
|
|
case "quit":
|
2020-09-01 22:18:43 -04:00
|
|
|
c.removeNickAllChans(GetNick(from))
|
2020-01-05 13:04:31 -05:00
|
|
|
if c.QuitCallback != nil {
|
|
|
|
c.QuitCallback(from, msg)
|
2020-01-05 12:39:58 -05:00
|
|
|
}
|
|
|
|
case "part":
|
2020-09-01 22:18:43 -04:00
|
|
|
c.removeNick(target, GetNick(from))
|
2020-01-05 13:04:31 -05:00
|
|
|
if c.PartCallback != nil {
|
|
|
|
c.PartCallback(from, target, msg)
|
2020-01-05 12:39:58 -05:00
|
|
|
}
|
2020-01-05 21:20:25 -05:00
|
|
|
case "nick":
|
2020-01-05 22:11:08 -05:00
|
|
|
if GetNick(from) == c.curNick {
|
2020-01-05 22:07:58 -05:00
|
|
|
c.curNick = msg
|
|
|
|
log.Printf("BOT NICK CHANGED TO: %s\n", c.curNick)
|
|
|
|
}
|
2020-01-05 21:20:25 -05:00
|
|
|
if c.NickCallback != nil {
|
2020-01-05 21:35:18 -05:00
|
|
|
c.NickCallback(from, msg)
|
2020-01-05 21:20:25 -05:00
|
|
|
}
|
2020-01-05 17:14:26 -05:00
|
|
|
default:
|
|
|
|
log.Printf("unhandled command: %s %s %s", cmd, target, msg)
|
2020-01-05 12:39:58 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//Run runs the irc event loop
|
2020-01-05 12:48:57 -05:00
|
|
|
func (c *Connection) Run() {
|
2020-01-05 12:39:58 -05:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|