blob: 88467167b1716a899127f49febe0f0d983c015f3 [file] [log] [blame]
package irc
import (
"context"
"fmt"
"net"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/golang/glog"
irc "gopkg.in/irc.v3"
)
// ircconn is a connection to IRC as a given user.
type ircconn struct {
// server to connect to
server string
// channel to join
channel string
// 'native' name of this connection.
user string
// Event Handler, usually a Manager
eventHandler func(e *event)
// TCP connection to IRC
conn net.Conn
// IRC client
irc *irc.Client
/// Fields used by the manager - do not access from ircconn.
// last time this connection was used
last time.Time
// is primary source of IRC data
receiver bool
// only exists to be a receiver
backup bool
// iq is the IRC Queue of IRC messages, populated by the IRC client and
// read by the connection.
iq chan *irc.Message
// sq is the Say Queue of controlMessages, populated by the Manager and
// read by the connection (and passed onto IRC)
sq chan *controlMessage
// eq is the Evict Queue, used by the manager to signal that a connection
// should die.
eq chan struct{}
// connected is a flag (via sync/atomic) that is used to signal to the
// manager that this connection is up and healthy.
connected int64
}
var reIRCNick = regexp.MustCompile(`[^A-Za-z0-9]`)
// Say is called by the Manager when a message should be sent out by the
// connection.
func (i *ircconn) Say(msg *controlMessage) {
i.sq <- msg
}
// Evict is called by the Manager when a connection should die.
func (i *ircconn) Evict() {
close(i.eq)
}
// ircMessage is a message received on IRC by a connection, sent over to the
// Manager.
type IRCMessage struct {
conn *ircconn
nick string
text string
}
func NewConn(server, channel, userTelegram string, backup bool, h func(e *event)) (*ircconn, error) {
// Generate IRC nick from username.
nick := reIRCNick.ReplaceAllString(userTelegram, "")
username := nick
if len(username) > 9 {
username = username[:9]
}
nick = strings.ToLower(nick)
if len(nick) > 13 {
nick = nick[:13]
}
if len(nick) == 0 {
glog.Errorf("Could not create IRC nick for %q", userTelegram)
nick = "wtf"
}
nick += "[t]"
glog.Infof("Connecting to IRC/%s/%s/%s as %s from %s...", server, channel, userTelegram, nick, username)
conn, err := net.Dial("tcp", server)
if err != nil {
return nil, fmt.Errorf("Dial(_, %q): %v", server, err)
}
i := &ircconn{
server: server,
channel: channel,
user: userTelegram,
eventHandler: h,
conn: conn,
irc: nil,
last: time.Now(),
backup: backup,
receiver: backup,
iq: make(chan *irc.Message),
sq: make(chan *controlMessage),
eq: make(chan struct{}),
connected: int64(0),
}
// Configure IRC client to populate the IRC Queue.
config := irc.ClientConfig{
Nick: nick,
User: username,
Name: userTelegram,
Handler: irc.HandlerFunc(func(c *irc.Client, m *irc.Message) {
i.iq <- m
}),
}
i.irc = irc.NewClient(conn, config)
return i, nil
}
func (i *ircconn) Run(ctx context.Context) {
var wg sync.WaitGroup
wg.Add(2)
go func() {
i.loop(ctx)
wg.Done()
}()
go func() {
err := i.irc.RunContext(ctx)
if err != ctx.Err() {
glog.Errorf("IRC/%s/%s/%s exited: %v", i.server, i.channel, i.user, err)
i.conn.Close()
i.eventHandler(&event{
dead: &eventDead{i},
})
}
wg.Wait()
}()
wg.Wait()
}
// IsConnected returns whether a connection is fully alive and able to receive
// messages.
func (i *ircconn) IsConnected() bool {
return atomic.LoadInt64(&i.connected) > 0
}
// loop is the main loop of an IRC connection.
// It synchronizes the Handler Queue, Say Queue and Evict Queue, parses
func (i *ircconn) loop(ctx context.Context) {
sayqueue := []*controlMessage{}
connected := false
dead := false
die := func(err error) {
// drain queue of say messages...
for _, s := range sayqueue {
glog.Infof("IRC/%s/say: [drop] %q", i.user, s.message)
s.done <- err
}
sayqueue = []*controlMessage{}
dead = true
i.conn.Close()
go i.eventHandler(&event{
dead: &eventDead{i},
})
}
msg := func(s *controlMessage) {
lines := strings.Split(s.message, "\n")
for _, l := range lines {
l = strings.TrimSpace(l)
if l == "" {
continue
}
err := i.irc.WriteMessage(&irc.Message{
Command: "PRIVMSG",
Params: []string{
i.channel,
l,
},
})
if err != nil {
glog.Errorf("IRC/%s: WriteMessage: %v", i.user, err)
die(err)
s.done <- err
return
}
}
s.done <- nil
}
// Timeout ticker - give up connecting to IRC after 15 seconds.
t := time.NewTicker(time.Second * 15)
previousNick := ""
for {
select {
case <-ctx.Done():
return
case <-i.eq:
glog.Infof("IRC/%s/info: got evicted", i.user)
die(fmt.Errorf("evicted"))
return
case m := <-i.iq:
if m.Command != "372" {
glog.V(1).Infof("IRC/%s/debug: %+v", i.user, m)
}
glog.V(16).Infof("irc/debug16: Message: cmd(%s), channel(%s)", m.Command, m.Params[0])
glog.V(16).Infof("irc/debug16: Current: channel(%s), command(%s)", i.channel, "PRIVMSG")
glog.V(16).Infof("irc/debug16: Current: channel-eq(%t), command-eq(%t)", i.channel == m.Params[0], "PRIVMSG" == m.Command)
switch {
case m.Command == "001":
glog.Infof("IRC/%s/info: joining %s...", i.user, i.channel)
i.irc.Write("JOIN " + i.channel)
case m.Command == "353":
glog.Infof("IRC/%s/info: joined and ready", i.user)
connected = true
atomic.StoreInt64(&i.connected, 1)
// drain queue of say messages...
for _, s := range sayqueue {
glog.Infof("IRC/%s/say: [backlog] %q", i.user, s.message)
msg(s)
}
sayqueue = []*controlMessage{}
case m.Command == "474":
// We are banned! :(
glog.Infof("IRC/%s/info: banned!", i.user)
go i.eventHandler(&event{
banned: &eventBanned{i},
})
die(nil)
return
case m.Command == "KICK" && m.Params[1] == i.irc.CurrentNick():
glog.Infof("IRC/%s/info: got kicked", i.user)
die(nil)
return
case m.Command == "PRIVMSG" && strings.ToLower(m.Params[0]) == i.channel:
glog.V(8).Infof("IRC/%s/debug8: received message on %s", i.user, i.channel)
go i.eventHandler(&event{
message: &eventMessage{i, m.Prefix.Name, m.Params[1]},
})
}
// update nickmap if needed
nick := i.irc.CurrentNick()
if previousNick != nick {
i.eventHandler(&event{
nick: &eventNick{i, nick},
})
previousNick = nick
}
case s := <-i.sq:
if dead {
glog.Infof("IRC/%s/say: [DEAD] %q", i.user, s.message)
s.done <- fmt.Errorf("connection is dead")
} else if connected {
glog.Infof("IRC/%s/say: %s", i.user, s.message)
msg(s)
} else {
glog.Infof("IRC/%s/say: [writeback] %q", i.user, s.message)
sayqueue = append(sayqueue, s)
}
case <-t.C:
if !connected {
glog.Errorf("IRC/%s/info: connection timed out, dying", i.user)
die(fmt.Errorf("connection timeout"))
return
}
}
}
}