blob: 9f1f4ef220e246d7dc69b427dc15533a6bca51e2 [file] [log] [blame]
Sergiusz Bazanskia8854882020-01-05 00:34:38 +01001package irc
2
3import (
4 "context"
5 "fmt"
6 "net"
Sergiusz Bazanski83e26902020-01-23 14:18:25 +01007 "regexp"
Sergiusz Bazanskia8854882020-01-05 00:34:38 +01008 "strings"
9 "sync"
10 "sync/atomic"
11 "time"
12
13 "github.com/golang/glog"
14 irc "gopkg.in/irc.v3"
15)
16
17// ircconn is a connection to IRC as a given user.
18type ircconn struct {
19 // server to connect to
20 server string
21 // channel to join
22 channel string
23 // 'native' name of this connection.
24 user string
25
26 // Event Handler, usually a Manager
27 eventHandler func(e *event)
28
29 // TCP connection to IRC
30 conn net.Conn
31 // IRC client
32 irc *irc.Client
33
34 /// Fields used by the manager - do not access from ircconn.
35 // last time this connection was used
36 last time.Time
37 // is primary source of IRC data
38 receiver bool
39 // only exists to be a receiver
40 backup bool
41 // iq is the IRC Queue of IRC messages, populated by the IRC client and
42 // read by the connection.
43 iq chan *irc.Message
44 // sq is the Say Queue of controlMessages, populated by the Manager and
45 // read by the connection (and passed onto IRC)
46 sq chan *controlMessage
47 // eq is the Evict Queue, used by the manager to signal that a connection
48 // should die.
49 eq chan struct{}
50
51 // connected is a flag (via sync/atomic) that is used to signal to the
52 // manager that this connection is up and healthy.
53 connected int64
54}
55
Sergiusz Bazanski83e26902020-01-23 14:18:25 +010056var reIRCNick = regexp.MustCompile(`[^a-z0-09]`)
57
Sergiusz Bazanskia8854882020-01-05 00:34:38 +010058// Say is called by the Manager when a message should be sent out by the
59// connection.
60func (i *ircconn) Say(msg *controlMessage) {
61 i.sq <- msg
62}
63
64// Evict is called by the Manager when a connection should die.
65func (i *ircconn) Evict() {
66 close(i.eq)
67}
68
69// ircMessage is a message received on IRC by a connection, sent over to the
70// Manager.
71type IRCMessage struct {
72 conn *ircconn
73 nick string
74 text string
75}
76
77func NewConn(server, channel, user string, backup bool, h func(e *event)) (*ircconn, error) {
78 glog.Infof("Connecting to IRC/%s/%s/%s...", server, channel, user)
79 conn, err := net.Dial("tcp", server)
80 if err != nil {
81 return nil, fmt.Errorf("Dial(_, %q): %v", server, err)
82 }
83
84 i := &ircconn{
85 server: server,
86 channel: channel,
87 user: user,
88
89 eventHandler: h,
90
91 conn: conn,
92 irc: nil,
93
94 last: time.Now(),
95 backup: backup,
96 receiver: backup,
97
98 iq: make(chan *irc.Message),
99 sq: make(chan *controlMessage),
100 eq: make(chan struct{}),
101
102 connected: int64(0),
103 }
104
105 // Generate IRC nick from username.
Sergiusz Bazanski83e26902020-01-23 14:18:25 +0100106 nick := reIRCNick.ReplaceAllString(user, "")
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100107 if len(nick) > 13 {
108 nick = nick[:13]
109 }
Sergiusz Bazanski83e26902020-01-23 14:18:25 +0100110 if len(nick) == 0 {
111 glog.Errorf("Could not create IRC nick for %q", user)
112 nick = "wtf"
113 }
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100114 nick += "[t]"
115
116 // Configure IRC client to populate the IRC Queue.
117 config := irc.ClientConfig{
118 Nick: nick,
119 User: user,
120 Name: user,
121 Handler: irc.HandlerFunc(func(c *irc.Client, m *irc.Message) {
122 i.iq <- m
123 }),
124 }
125
126 i.irc = irc.NewClient(conn, config)
127 return i, nil
128}
129
130func (i *ircconn) Run(ctx context.Context) {
131 var wg sync.WaitGroup
132 wg.Add(2)
133
134 go func() {
135 i.loop(ctx)
136 wg.Done()
137 }()
138
139 go func() {
140 err := i.irc.RunContext(ctx)
141 if err != ctx.Err() {
142 glog.Errorf("IRC/%s/%s/%s exited: %v", i.server, i.channel, i.user, err)
143 i.conn.Close()
144 i.eventHandler(&event{
145 dead: &eventDead{i},
146 })
147 }
148 wg.Wait()
149 }()
150
151 wg.Wait()
152}
153
154// IsConnected returns whether a connection is fully alive and able to receive
155// messages.
156func (i *ircconn) IsConnected() bool {
157 return atomic.LoadInt64(&i.connected) > 0
158}
159
160// loop is the main loop of an IRC connection.
161// It synchronizes the Handler Queue, Say Queue and Evict Queue, parses
162func (i *ircconn) loop(ctx context.Context) {
163 sayqueue := []*controlMessage{}
164 connected := false
165 dead := false
166
Sergiusz Bazanski93773132020-01-22 21:47:25 +0100167 die := func(err error) {
168 // drain queue of say messages...
169 for _, s := range sayqueue {
170 glog.Infof("IRC/%s/say: [drop] %q", i.user, s.message)
171 s.done <- err
172 }
173 sayqueue = []*controlMessage{}
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100174 dead = true
175 i.conn.Close()
176 go i.eventHandler(&event{
177 dead: &eventDead{i},
178 })
179 }
180 msg := func(s *controlMessage) {
181 lines := strings.Split(s.message, "\n")
182 for _, l := range lines {
183 l = strings.TrimSpace(l)
184 if l == "" {
185 continue
186 }
187 err := i.irc.WriteMessage(&irc.Message{
188 Command: "PRIVMSG",
189 Params: []string{
190 i.channel,
191 l,
192 },
193 })
194 if err != nil {
195 glog.Errorf("IRC/%s: WriteMessage: %v", i.user, err)
Sergiusz Bazanski83e26902020-01-23 14:18:25 +0100196 die(err)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100197 s.done <- err
198 return
199 }
200 }
201 s.done <- nil
202 }
203
204 // Timeout ticker - give up connecting to IRC after 15 seconds.
205 t := time.NewTicker(time.Second * 15)
206
207 previousNick := ""
208
209 for {
210 select {
211 case <-ctx.Done():
212 return
213
214 case <-i.eq:
215 glog.Infof("IRC/%s/info: got evicted", i.user)
Sergiusz Bazanski93773132020-01-22 21:47:25 +0100216 die(fmt.Errorf("evicted"))
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100217 return
218
219 case m := <-i.iq:
220 if m.Command != "372" {
221 glog.V(1).Infof("IRC/%s/debug: %+v", i.user, m)
222 }
223
224 switch {
225 case m.Command == "001":
226 glog.Infof("IRC/%s/info: joining %s...", i.user, i.channel)
227 i.irc.Write("JOIN " + i.channel)
228
229 case m.Command == "353":
230 glog.Infof("IRC/%s/info: joined and ready", i.user)
231 connected = true
232 atomic.StoreInt64(&i.connected, 1)
233 // drain queue of say messages...
234 for _, s := range sayqueue {
235 glog.Infof("IRC/%s/say: [backlog] %q", i.user, s.message)
236 msg(s)
237 }
238 sayqueue = []*controlMessage{}
239
240 case m.Command == "474":
241 // We are banned! :(
242 glog.Infof("IRC/%s/info: banned!", i.user)
243 go i.eventHandler(&event{
244 banned: &eventBanned{i},
245 })
Sergiusz Bazanski93773132020-01-22 21:47:25 +0100246 die(nil)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100247 return
248
249 case m.Command == "KICK" && m.Params[1] == i.irc.CurrentNick():
250 glog.Infof("IRC/%s/info: got kicked", i.user)
Sergiusz Bazanski93773132020-01-22 21:47:25 +0100251 die(nil)
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100252 return
253
254 case m.Command == "PRIVMSG" && m.Params[0] == i.channel:
255 go i.eventHandler(&event{
256 message: &eventMessage{i, m.Prefix.Name, m.Params[1]},
257 })
258 }
259
260 // update nickmap if needed
261 nick := i.irc.CurrentNick()
262 if previousNick != nick {
263 i.eventHandler(&event{
264 nick: &eventNick{i, nick},
265 })
266 previousNick = nick
267 }
268
269 case s := <-i.sq:
270 if dead {
271 glog.Infof("IRC/%s/say: [DEAD] %q", i.user, s.message)
272 s.done <- fmt.Errorf("connection is dead")
273 } else if connected {
274 glog.Infof("IRC/%s/say: %s", i.user, s.message)
275 msg(s)
276 } else {
277 glog.Infof("IRC/%s/say: [writeback] %q", i.user, s.message)
278 sayqueue = append(sayqueue, s)
279 }
280
281 case <-t.C:
282 if !connected {
283 glog.Errorf("IRC/%s/info: connection timed out, dying", i.user)
Sergiusz Bazanski93773132020-01-22 21:47:25 +0100284 die(fmt.Errorf("connection timeout"))
Sergiusz Bazanskia8854882020-01-05 00:34:38 +0100285 return
286 }
287 }
288 }
289}