gorepost/irc/irc.go

213 lines
5.3 KiB
Go

// Copyright 2015 Robert S. Gerus. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package irc
import (
"bufio"
"log"
"math/rand"
"net"
"sync"
"time"
"github.com/arachnist/dyncfg"
)
const delim byte = '\n'
const endline string = "\r\n"
// Connection struct. Contains basic information about this connection, and quit
// channels.
type Connection struct {
network string
reader *bufio.Reader
writer *bufio.Writer
dispatcher func(func(Message), Message)
conn net.Conn
reconnect chan struct{}
reconnectCleanup chan struct{}
Quit chan struct{}
quitrecv chan struct{}
quitkeeper chan struct{}
l sync.Mutex
cfg *dyncfg.Dyncfg
}
// Sender sends IRC messages to server and logs their contents.
func (c *Connection) Sender(msg Message) {
c.l.Lock()
defer c.l.Unlock()
c.writer.WriteString(msg.String() + endline)
log.Println(c.network, "-->", msg.String())
c.writer.Flush()
}
// Receiver receives IRC messages from server, logs their contents, sets message
// context and initializes disconnect procedure on timeout or other errors.
func (c *Connection) Receiver() {
log.Println(c.network, "spawned Receiver")
for {
c.conn.SetDeadline(time.Now().Add(time.Second * 600))
raw, err := c.reader.ReadString(delim)
var src, tgt string
if err != nil {
log.Println(c.network, "error reading message", err.Error())
log.Println(c.network, "closing Receiver")
c.reconnectCleanup <- struct{}{}
log.Println(c.network, "sent reconnect message from Receiver")
return
}
msg, err := ParseMessage(raw)
if err != nil {
log.Println(c.network, "error decoding message", err.Error())
log.Println(c.network, "closing Receiver")
c.reconnectCleanup <- struct{}{}
log.Println(c.network, "sent reconnect message from Receiver")
return
}
log.Println(c.network, "<--", msg.String())
if msg.Params == nil {
tgt = ""
} else {
tgt = msg.Params[0]
}
if msg.Prefix == nil {
src = ""
} else {
src = msg.Prefix.Name
}
msg.Context = map[string]string{
"Network": c.network,
"Source": src,
"Target": tgt,
}
c.dispatcher(c.Sender, *msg)
select {
case <-c.quitrecv:
log.Println(c.network, "closing Receiver")
return
default:
}
}
}
// Cleaner cleans up coroutines on IRC connection errors and initializes
// reconnection.
func (c *Connection) Cleaner() {
log.Println(c.network, "spawned Cleaner")
for {
select {
case <-c.Quit:
log.Println(c.network, "closing connection")
c.l.Lock()
defer c.l.Unlock()
log.Println(c.network, "cleaning up!")
c.quitrecv <- struct{}{}
// there's a slight chance to hit this if quit request is received
// before irc connection is established, possibly between reconnects
if c.conn != nil {
c.conn.Close()
}
log.Println(c.network, "closing Cleaner")
return
case <-c.reconnectCleanup:
log.Println(c.network, "cleaning up before reconnect!")
c.l.Lock()
log.Println(c.network, "cleaning up!")
c.quitrecv <- struct{}{}
c.conn.Close()
log.Println(c.network, "sending reconnect signal!")
c.l.Unlock()
c.reconnect <- struct{}{}
}
}
}
// Keeper makes sure that IRC connection is alive by reconnecting when
// requested and restarting Receiver goroutine.
func (c *Connection) Keeper() {
log.Println(c.network, "spawned Keeper")
context := make(map[string]string)
context["Network"] = c.network
for {
select {
case <-c.quitkeeper:
if c.quitrecv != nil {
close(c.quitrecv)
}
return
case <-c.reconnect:
}
c.l.Lock()
if c.quitrecv != nil {
close(c.quitrecv)
}
c.quitrecv = make(chan struct{}, 1)
servers := c.cfg.LookupStringSlice(context, "Servers")
server := servers[rand.Intn(len(servers))]
log.Println(c.network, "connecting to", server)
err := c.Dial(server)
c.l.Unlock()
if err == nil {
go c.Receiver()
log.Println(c.network, "Initializing IRC connection")
c.Sender(Message{
Command: "NICK",
Trailing: c.cfg.LookupString(context, "Nick"),
})
c.Sender(Message{
Command: "USER",
Params: []string{c.cfg.LookupString(context, "User"), "0", "*"},
Trailing: c.cfg.LookupString(context, "RealName"),
})
} else {
log.Println(c.network, "connection error", err.Error())
time.Sleep(time.Second * 3)
c.reconnect <- struct{}{}
}
}
}
// Setup performs initialization tasks.
func (c *Connection) Setup(dispatcher func(func(Message), Message), network string, config *dyncfg.Dyncfg) {
rand.Seed(time.Now().UnixNano())
c.reconnect = make(chan struct{}, 1)
c.reconnectCleanup = make(chan struct{}, 1)
c.quitkeeper = make(chan struct{}, 1)
c.Quit = make(chan struct{}, 1)
c.network = network
c.dispatcher = dispatcher
c.cfg = config
c.reconnect <- struct{}{}
go c.Keeper()
go c.Cleaner()
return
}
// Dial connects to irc server and sets up bufio reader and writer.
func (c *Connection) Dial(server string) error {
conn, err := net.DialTimeout("tcp", server, time.Second*30)
if err != nil {
log.Println(c.network, "Cannot connect to", server, "error:", err.Error())
return err
}
log.Println(c.network, "Connected to", server)
c.writer = bufio.NewWriter(conn)
c.reader = bufio.NewReader(conn)
c.conn = conn
return nil
}