forked from hswaw/hscloud
300 lines
6.8 KiB
Go
300 lines
6.8 KiB
Go
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
|
|
}
|
|
}
|
|
}
|
|
}
|